diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index d2d3f6b80..db642b372 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -24,7 +24,7 @@ env: jobs: build: name: Sanity Build - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v3 @@ -33,12 +33,12 @@ jobs: # --------------------------------------- # Install .NET Versions # --------------------------------------- - - uses: actions/setup-dotnet@v2 + - uses: actions/setup-dotnet@v4 name: Install .NET with: dotnet-version: | - 6.x - 7.x + 8.x + 9.x # --------------------------------------- # Configure the build environment diff --git a/.github/workflows/nuget-deployment.yml b/.github/workflows/nuget-deployment.yml index 2754b0b5c..02cecd95f 100644 --- a/.github/workflows/nuget-deployment.yml +++ b/.github/workflows/nuget-deployment.yml @@ -26,7 +26,7 @@ env: jobs: deployment: name: Pack & Deploy - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v3 @@ -35,12 +35,12 @@ jobs: # --------------------------------------- # Install .NET Versions # --------------------------------------- - - uses: actions/setup-dotnet@v2 + - uses: actions/setup-dotnet@v4 name: Install .NET with: dotnet-version: | - 6.x - 7.x + 8.x + 9.x # --------------------------------------- # Configure the build environment @@ -77,7 +77,7 @@ jobs: # --------------------------------------- # Save Artifacts for later debugging # --------------------------------------- - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 name: Save Artifacts with: name: generated-nuget-packages diff --git a/README.md b/README.md index 2efd6a684..c29109c62 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ### Documentation: [https://graphql-aspnet.github.io](https://graphql-aspnet.github.io) -> Targets: **netstandard2.0, net6.0, net7.0** +> Targets: **netstandard2.0, net6.0, net8.0** [![CI-CD](https://github.com/graphql-aspnet/graphql-aspnet/actions/workflows/ci-build.yml/badge.svg?branch=master)](https://github.com/graphql-aspnet/graphql-aspnet/actions/workflows/ci-build.yml) diff --git a/src/.editorconfig b/src/.editorconfig index 55608d4fe..c2d30edfd 100644 --- a/src/.editorconfig +++ b/src/.editorconfig @@ -197,7 +197,7 @@ dotnet_naming_style.begins_with_i.word_separator = dotnet_naming_style.begins_with_i.capitalization = pascal_case - - [*.{cs,vb}] -dotnet_style_prefer_compound_assignment=true:suggestion \ No newline at end of file +dotnet_style_prefer_compound_assignment=true:suggestion + +file_header_template = *************************************************************\n project: graphql-aspnet\n --\n repo: https://github.com/graphql-aspnet\n docs: https://graphql-aspnet.github.io\n --\n License: MIT\n ************************************************************* \ No newline at end of file diff --git a/src/ancillary-projects/benchmarking/graphql-aspnet-benchmarks/graphql-aspnet-benchmarks.csproj b/src/ancillary-projects/benchmarking/graphql-aspnet-benchmarks/graphql-aspnet-benchmarks.csproj index a6be044cf..c3cec3248 100644 --- a/src/ancillary-projects/benchmarking/graphql-aspnet-benchmarks/graphql-aspnet-benchmarks.csproj +++ b/src/ancillary-projects/benchmarking/graphql-aspnet-benchmarks/graphql-aspnet-benchmarks.csproj @@ -1,7 +1,7 @@  - net7.0 + net8.0 latest $(NoWarn);1701;1702;1705;1591;NU1603;IDE0060;IDE0052;IDE0044;IDE0059;IDE0052;IDE0017;IDE0039 GraphQL.AspNet.Benchmarks diff --git a/src/ancillary-projects/benchmarking/graphql-aspnet-load-console/graphql-aspnet-load-console.csproj b/src/ancillary-projects/benchmarking/graphql-aspnet-load-console/graphql-aspnet-load-console.csproj index c3b87865e..9861d8e15 100644 --- a/src/ancillary-projects/benchmarking/graphql-aspnet-load-console/graphql-aspnet-load-console.csproj +++ b/src/ancillary-projects/benchmarking/graphql-aspnet-load-console/graphql-aspnet-load-console.csproj @@ -2,7 +2,7 @@ Exe - net7.0 + net8.0 GraphQL.AspNet.SubscriberLoadTest.TestConsole GraphQL ASP.NET GraphQL ASP.NET Performance Profiler diff --git a/src/ancillary-projects/benchmarking/graphql-aspnet-load-models/graphql-aspnet-load-models.csproj b/src/ancillary-projects/benchmarking/graphql-aspnet-load-models/graphql-aspnet-load-models.csproj index 3fec40e2d..c9d77d3d4 100644 --- a/src/ancillary-projects/benchmarking/graphql-aspnet-load-models/graphql-aspnet-load-models.csproj +++ b/src/ancillary-projects/benchmarking/graphql-aspnet-load-models/graphql-aspnet-load-models.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 GraphQL.AspNet.SubscriberLoadTest.Models true diff --git a/src/ancillary-projects/benchmarking/graphql-aspnet-load-server/graphql-aspnet-load-server.csproj b/src/ancillary-projects/benchmarking/graphql-aspnet-load-server/graphql-aspnet-load-server.csproj index 2705b4657..72585745c 100644 --- a/src/ancillary-projects/benchmarking/graphql-aspnet-load-server/graphql-aspnet-load-server.csproj +++ b/src/ancillary-projects/benchmarking/graphql-aspnet-load-server/graphql-aspnet-load-server.csproj @@ -1,7 +1,7 @@  - net7.0 + net8.0 GraphQL.AspNet.SubscriberLoadTest.Server true diff --git a/src/ancillary-projects/starwars/starwars-api60/Program.cs b/src/ancillary-projects/starwars/starwars-api60/Program.cs deleted file mode 100644 index 7c02ad43e..000000000 --- a/src/ancillary-projects/starwars/starwars-api60/Program.cs +++ /dev/null @@ -1,29 +0,0 @@ -// ************************************************************* -// project: graphql-aspnet -// -- -// repo: https://github.com/graphql-aspnet -// docs: https://graphql-aspnet.github.io -// -- -// License: MIT -// ************************************************************* - -namespace GraphQL.AspNet.StarWarsAPI6X -{ - using Microsoft.AspNetCore.Hosting; - using Microsoft.Extensions.Hosting; - - public static class Program - { - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } - - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); - } -} \ No newline at end of file diff --git a/src/ancillary-projects/starwars/starwars-api60/Properties/launchSettings.json b/src/ancillary-projects/starwars/starwars-api60/Properties/launchSettings.json deleted file mode 100644 index 18f8355d2..000000000 --- a/src/ancillary-projects/starwars/starwars-api60/Properties/launchSettings.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "$schema": "http://json.schemastore.org/launchsettings.json", - "profiles": { - "StarWars: .NET 6": { - "commandName": "Project", - "applicationUrl": "http://localhost:5000", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - } - } -} diff --git a/src/ancillary-projects/starwars/starwars-api70/Program.cs b/src/ancillary-projects/starwars/starwars-api70/Program.cs deleted file mode 100644 index ddd499734..000000000 --- a/src/ancillary-projects/starwars/starwars-api70/Program.cs +++ /dev/null @@ -1,29 +0,0 @@ -// ************************************************************* -// project: graphql-aspnet -// -- -// repo: https://github.com/graphql-aspnet -// docs: https://graphql-aspnet.github.io -// -- -// License: MIT -// ************************************************************* - -namespace GraphQL.AspNet.StarWarsAPI7X -{ - using Microsoft.AspNetCore.Hosting; - using Microsoft.Extensions.Hosting; - - public static class Program - { - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } - - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); - } -} \ No newline at end of file diff --git a/src/ancillary-projects/starwars/starwars-api70/Startup.cs b/src/ancillary-projects/starwars/starwars-api70/Startup.cs deleted file mode 100644 index 94500545e..000000000 --- a/src/ancillary-projects/starwars/starwars-api70/Startup.cs +++ /dev/null @@ -1,171 +0,0 @@ -// ************************************************************* -// project: graphql-aspnet -// -- -// repo: https://github.com/graphql-aspnet -// docs: https://graphql-aspnet.github.io -// -- -// License: MIT -// ************************************************************* - -namespace GraphQL.AspNet.StarWarsAPI7X -{ - using System; - using GraphQL.AspNet; - using GraphQL.AspNet.Configuration; - using GraphQL.AspNet.Execution; - using GraphQL.AspNet.StarwarsAPI.Common.Services; - using Microsoft.AspNetCore.Builder; - using Microsoft.AspNetCore.Hosting; - using Microsoft.AspNetCore.WebSockets; - using Microsoft.Extensions.Configuration; - using Microsoft.Extensions.DependencyInjection; - - public class Startup - { - private const string ALL_ORIGINS_POLICY = "_allOrigins"; - - private static readonly TimeSpan SOCKET_CONNECTION_KEEPALIVE = TimeSpan.FromSeconds(10); - - /// - /// Initializes a new instance of the class. - /// - /// The configuration created to govern the - /// application environment. - public Startup(IConfiguration configuration) - { - this.Configuration = configuration; - } - - /// - /// Configures the service collection to be built for this application instance. - /// - /// The services. - public void ConfigureServices(IServiceCollection services) - { - services.AddSingleton(); - services.AddScoped(); - - // apply an unrestricted cors policy for the demo services - // to allow use on many of the tools for testing (graphiql, altair etc.) - // Do not do this in production - services.AddCors(options => - { - options.AddPolicy( - ALL_ORIGINS_POLICY, - builder => - { - builder.AllowAnyOrigin() - .AllowAnyHeader() - .AllowAnyMethod(); - }); - }); - - // ASP.NET websockets implementation must also be added to the runtime - services.AddWebSockets((options) => - { - // here add some common origins of various tools that may be - // used for running this demo - // do not add these in a production app unless you need - // to - options.AllowedOrigins.Add("http://localhost:5000"); - options.AllowedOrigins.Add("http://localhost:4000"); - options.AllowedOrigins.Add("http://localhost:3000"); - options.AllowedOrigins.Add("null"); - options.AllowedOrigins.Add("file://"); - options.AllowedOrigins.Add("ws://"); - - }); - - // ---------------------------------------------------------- - // Register GraphQL with the application - // ---------------------------------------------------------- - // By default graphql will scan your assembly for any GraphControllers - // and automatically wire them up to the schema - // you can control which assemblies are scanned and which classes are registered using - // the schema configuration options set here. - // - // in this example because of the two test projects (netcore3.1 and net5.0) - // we have moved all the shared code to a common assembly (starwars-common) and are injecting it - // as a single unit - // - // we then add subscription services to the schema builder returned from .AddGraphQL() - services.AddGraphQL(options => - { - options.ResponseOptions.ExposeExceptions = true; - options.ResponseOptions.MessageSeverityLevel = GraphMessageSeverity.Information; - - // options.ExecutionOptions.EnableMetrics = true; - // options.ResponseOptions.ExposeMetrics = true; - - var assembly = typeof(StarWarsDataRepository).Assembly; - options.AddAssembly(assembly); - }) - .AddSubscriptions(options => - { - // this route path is set by default - // it is listed here just as a matter of example - options.Route = SubscriptionConstants.Routing.DEFAULT_SUBSCRIPTIONS_ROUTE; - - // for some web based graphql tools such as graphiql and graphql-playground - // the default keep-alive timeout of 2 minutes is too long. - // - // still others (like graphql-playground running in electron) do not respond/configure - // for socket-level ping/pong frames to allow for socket-level keep alives - // - // here we set this demo project websocket keep-alive (at the server level) - // to be below all those thresholds to ensure a hassle free experience. - // In practice, you should configure your server (both subscription keep alives and socket keep alives) - // with an interval that is compatiable with your client side environment. - options.ConnectionKeepAliveInterval = SOCKET_CONNECTION_KEEPALIVE; - }); - - services.AddControllers(); - } - - /// - /// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - /// - /// The asp.net application builder. - /// The configured host environment. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - app.AddStarWarsStartedMessageToConsole(); - - app.UseRouting(); - - app.UseCors(ALL_ORIGINS_POLICY); - - app.UseAuthorization(); - - // enable web sockets on this server instance - // this must be done before a call to 'UseGraphQL' if subscriptions are enabled for any - // schema otherwise the subscriptions may not register correctly - app.UseWebSockets(); - - // if you have no rest controllers this item can be safely skipped - // graphql and rest can live side by side in the same project without issue - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); - - // ************************************************************ - // Finalize the graphql setup: - // 1) Loading the schema - // 2) Publish the route to hook the graphql runtime to ASP.NET. - // - // Be sure to register it after "UseAuthorization" if you need it. - // - // If the construction of your runtime schema has any errors they will be thrown here - // before your application starts listening for requests. - // ************************************************************ - app.UseGraphQL(); - } - - /// - /// Gets the environment configuration for this instance. - /// - /// The configuration item. - public IConfiguration Configuration { get; } - } -} \ No newline at end of file diff --git a/src/ancillary-projects/starwars/starwars-api70/appsettings.json b/src/ancillary-projects/starwars/starwars-api70/appsettings.json deleted file mode 100644 index 289add7ec..000000000 --- a/src/ancillary-projects/starwars/starwars-api70/appsettings.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Information", - "Microsoft.AspNetCore.Hosting.Internal.WebHost": "Debug", - "Microsoft.AspNetCore.Hosting.Diagnostics": "Debug", - "GraphQL.AspNet": "Warning" - } - }, - "AllowedHosts": "*" -} diff --git a/src/ancillary-projects/starwars/starwars-api70/starwars-api70.csproj b/src/ancillary-projects/starwars/starwars-api70/starwars-api70.csproj deleted file mode 100644 index a9df881c7..000000000 --- a/src/ancillary-projects/starwars/starwars-api70/starwars-api70.csproj +++ /dev/null @@ -1,26 +0,0 @@ - - - - net7.0 - latest - $(NoWarn);1701;1702;1705;1591;NU1603 - GraphQL.AspNet.StarWarsAPI7X - GraphQL.AspNet.StarwarsAPI7X - true - - - - ..\..\..\styles.ruleset - - - - - - - - - - - - - diff --git a/src/ancillary-projects/starwars/starwars-api60/Startup.cs b/src/ancillary-projects/starwars/starwars-api90/Program.cs similarity index 77% rename from src/ancillary-projects/starwars/starwars-api60/Startup.cs rename to src/ancillary-projects/starwars/starwars-api90/Program.cs index b8f2d4f39..c04447664 100644 --- a/src/ancillary-projects/starwars/starwars-api60/Startup.cs +++ b/src/ancillary-projects/starwars/starwars-api90/Program.cs @@ -10,44 +10,33 @@ namespace GraphQL.AspNet.StarWarsAPI6X { using System; - using GraphQL.AspNet; using GraphQL.AspNet.Configuration; using GraphQL.AspNet.Execution; using GraphQL.AspNet.StarwarsAPI.Common.Services; using Microsoft.AspNetCore.Builder; - using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.WebSockets; - using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; - public class Startup + public static class Program { private const string ALL_ORIGINS_POLICY = "_allOrigins"; private static readonly TimeSpan SOCKET_CONNECTION_KEEPALIVE = TimeSpan.FromSeconds(10); - /// - /// Initializes a new instance of the class. - /// - /// The configuration. - public Startup(IConfiguration configuration) + public static void Main(string[] args) { - this.Configuration = configuration; - } + var builder = WebApplication.CreateBuilder(args); - /// - /// Configures the service collection to be built for this application instance. - /// - /// The services. - public void ConfigureServices(IServiceCollection services) - { - services.AddSingleton(); - services.AddScoped(); + // Add various required services + // ---------------------------------------------- + builder.Services.AddAuthorization(); + builder.Services.AddSingleton(); + builder.Services.AddScoped(); // apply an unrestricted cors policy for the demo services // to allow use on many of the tools for testing (graphiql, altair etc.) // Do not do this in production - services.AddCors(options => + builder.Services.AddCors(options => { options.AddPolicy( ALL_ORIGINS_POLICY, @@ -72,7 +61,7 @@ public void ConfigureServices(IServiceCollection services) // as a single unit // // we then add subscription services to the schema builder returned from .AddGraphQL() - services.AddGraphQL(options => + builder.Services.AddGraphQL(options => { options.ResponseOptions.ExposeExceptions = true; options.ResponseOptions.MessageSeverityLevel = GraphMessageSeverity.Information; @@ -102,11 +91,13 @@ public void ConfigureServices(IServiceCollection services) options.ConnectionKeepAliveInterval = SOCKET_CONNECTION_KEEPALIVE; }); - services.AddControllers(); - + // if you have rest controllers this item be sure they are included. + // Graphql and rest can live side by side in the same project without issue + // -------------------------------------------------- + // builder.Services.AddControllers(); // ASP.NET websockets implementation must also be added to the runtime - services.AddWebSockets((options) => + builder.Services.AddWebSockets((options) => { // here add some common origins of various tools that may be // used for running this demo @@ -121,23 +112,14 @@ public void ConfigureServices(IServiceCollection services) // do not add these in a production app options.AllowedOrigins.Add("file://"); options.AllowedOrigins.Add("ws://"); - }); - } - /// - /// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - /// - /// The asp.net application builder. - /// The configured host environment. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - app.AddStarWarsStartedMessageToConsole(); + var app = builder.Build(); + // Configure the HTTP request pipeline. + app.AddStarWarsStartedMessageToConsole(); app.UseRouting(); - app.UseCors(ALL_ORIGINS_POLICY); - app.UseAuthorization(); // enable web sockets on this server instance @@ -147,10 +129,11 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) // if you have no rest controllers this item can be safely skipped // graphql and rest can live side by side in the same project without issue - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); + // ----------------------------------------------------------------- + // app.UseEndpoints(endpoints => + // { + // endpoints.MapControllers(); + // }); // ************************************************************ // Finalize the graphql setup by loading the schema, build out the templates for all found graph types @@ -161,12 +144,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) // before your application starts listening for requests. // ************************************************************ app.UseGraphQL(); + app.Run(); } - - /// - /// Gets the configuration. - /// - /// The configuration. - public IConfiguration Configuration { get; } } } \ No newline at end of file diff --git a/src/ancillary-projects/starwars/starwars-api70/Properties/launchSettings.json b/src/ancillary-projects/starwars/starwars-api90/Properties/launchSettings.json similarity index 91% rename from src/ancillary-projects/starwars/starwars-api70/Properties/launchSettings.json rename to src/ancillary-projects/starwars/starwars-api90/Properties/launchSettings.json index 530c470fd..837d831d9 100644 --- a/src/ancillary-projects/starwars/starwars-api70/Properties/launchSettings.json +++ b/src/ancillary-projects/starwars/starwars-api90/Properties/launchSettings.json @@ -1,7 +1,7 @@ { "$schema": "http://json.schemastore.org/launchsettings.json", "profiles": { - "StarWars: .NET 7": { + "StarWars: .NET 9": { "commandName": "Project", "applicationUrl": "http://localhost:5000", "environmentVariables": { diff --git a/src/ancillary-projects/starwars/starwars-api60/appsettings.json b/src/ancillary-projects/starwars/starwars-api90/appsettings.json similarity index 100% rename from src/ancillary-projects/starwars/starwars-api60/appsettings.json rename to src/ancillary-projects/starwars/starwars-api90/appsettings.json diff --git a/src/ancillary-projects/starwars/starwars-api60/starwars-api60.csproj b/src/ancillary-projects/starwars/starwars-api90/starwars-api90.csproj similarity index 94% rename from src/ancillary-projects/starwars/starwars-api60/starwars-api60.csproj rename to src/ancillary-projects/starwars/starwars-api90/starwars-api90.csproj index c08b24c84..44771342b 100644 --- a/src/ancillary-projects/starwars/starwars-api60/starwars-api60.csproj +++ b/src/ancillary-projects/starwars/starwars-api90/starwars-api90.csproj @@ -1,7 +1,7 @@ - net6.0 + net9.0 latest $(NoWarn);1701;1702;1705;1591;NU1603 GraphQL.AspNet.StarWarsAPI6X diff --git a/src/ancillary-projects/starwars/starwars-common/starwars-common.csproj b/src/ancillary-projects/starwars/starwars-common/starwars-common.csproj index 4a9c2fff7..07a7716b1 100644 --- a/src/ancillary-projects/starwars/starwars-common/starwars-common.csproj +++ b/src/ancillary-projects/starwars/starwars-common/starwars-common.csproj @@ -1,7 +1,7 @@  - net7.0;net6.0;netstandard2.0; + net9.0;net8.0;netstandard2.0; latest $(NoWarn);1701;1702;1705;1591;NU1603 GraphQL.AspNet.StarwarsAPI.Common diff --git a/src/graphql-aspnet-subscriptions/SubscriptionServer/Protocols/GraphqlTransportWs/GqltwsClientProxy.cs b/src/graphql-aspnet-subscriptions/SubscriptionServer/Protocols/GraphqlTransportWs/GqltwsClientProxy.cs index 422325740..e023e335c 100644 --- a/src/graphql-aspnet-subscriptions/SubscriptionServer/Protocols/GraphqlTransportWs/GqltwsClientProxy.cs +++ b/src/graphql-aspnet-subscriptions/SubscriptionServer/Protocols/GraphqlTransportWs/GqltwsClientProxy.cs @@ -94,6 +94,7 @@ public GqltwsClientProxy( _serializerOptions.Converters.Add(new GqltwsServerDataMessageConverter(schema, responseWriter)); _serializerOptions.Converters.Add(new GqltwsServerCompleteMessageConverter()); _serializerOptions.Converters.Add(new GqltwsServerErrorMessageConverter(schema)); + _serializerOptions.Converters.Add(new GqltwsMessageConverter()); } /// diff --git a/src/graphql-aspnet-subscriptions/SubscriptionServer/Protocols/GraphqlWsLegacy/GraphqlWsLegacyClientProxy.cs b/src/graphql-aspnet-subscriptions/SubscriptionServer/Protocols/GraphqlWsLegacy/GraphqlWsLegacyClientProxy.cs index 0b4bc66e4..2c580f9bb 100644 --- a/src/graphql-aspnet-subscriptions/SubscriptionServer/Protocols/GraphqlWsLegacy/GraphqlWsLegacyClientProxy.cs +++ b/src/graphql-aspnet-subscriptions/SubscriptionServer/Protocols/GraphqlWsLegacy/GraphqlWsLegacyClientProxy.cs @@ -93,6 +93,7 @@ public GraphqlWsLegacyClientProxy( _serializeOptions.Converters.Add(new GraphqlWsLegacyServerDataMessageConverter(schema, responseWriter)); _serializeOptions.Converters.Add(new GraphqlWsLegacyServerCompleteMessageConverter()); _serializeOptions.Converters.Add(new GraphqlWsLegacyServerErrorMessageConverter(schema)); + _serializeOptions.Converters.Add(new GraphqlWsLegacyMessageConverter()); } /// @@ -305,7 +306,7 @@ private async Task AcknowledgeNewConnectionAsync() /// protected override async Task ExecuteKeepAliveAsync(CancellationToken cancelToken = default) { - await this.SendMessageAsync(new GraphqlWsLegacyKeepAliveOperationMessage()); + await this.SendMessageAsync(new GraphqlWsLegacyKeepAliveOperationMessage(), cancelToken); } /// diff --git a/src/graphql-aspnet-subscriptions/Web/WebSockets/WebSocketClientConnection.cs b/src/graphql-aspnet-subscriptions/Web/WebSockets/WebSocketClientConnection.cs index 2dc728677..e5cf9a73d 100644 --- a/src/graphql-aspnet-subscriptions/Web/WebSockets/WebSocketClientConnection.cs +++ b/src/graphql-aspnet-subscriptions/Web/WebSockets/WebSocketClientConnection.cs @@ -56,7 +56,7 @@ private string DeteremineRequestedProtocol() if (_httpContext.Request.Headers.ContainsKey(SubscriptionConstants.WebSockets.WEBSOCKET_PROTOCOL_HEADER)) { var protocolHeaders = _httpContext.Request.Headers[SubscriptionConstants.WebSockets.WEBSOCKET_PROTOCOL_HEADER]; - return string.Join(",", protocolHeaders); + return string.Join(",", protocolHeaders.ToArray()); } return string.Empty; diff --git a/src/graphql-aspnet.sln b/src/graphql-aspnet.sln index 22cdbb2e4..ac80d2c73 100644 --- a/src/graphql-aspnet.sln +++ b/src/graphql-aspnet.sln @@ -1,4 +1,3 @@ - Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31825.309 @@ -8,8 +7,10 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{4274961A-0C80-4332-80A7-3FDCB1DA2DA0}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig + .runsettings = .runsettings CodeMaid.config = CodeMaid.config library-common.props = library-common.props + library-tests.props = library-tests.props stylecop.json = stylecop.json styles.ruleset = styles.ruleset EndProjectSection @@ -26,15 +27,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "starwars-common", "ancillar EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "graphql-aspnet-tests", "unit-tests\graphql-aspnet-tests\graphql-aspnet-tests.csproj", "{5DE081AA-494A-4377-B2CA-6952715D513D}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "starwars-api60", "ancillary-projects\starwars\starwars-api60\starwars-api60.csproj", "{BAC0B33E-CA4D-4238-9E27-FDFC308440FC}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "graphql-aspnet-subscriptions", "graphql-aspnet-subscriptions\graphql-aspnet-subscriptions.csproj", "{CA9D8DEC-13D6-4ED0-9CDC-747C2B5BCFB4}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "graphql-aspnet-subscriptions-tests", "unit-tests\graphql-aspnet-subscriptions-tests\graphql-aspnet-subscriptions-tests.csproj", "{6E4A16F5-1B98-412E-9A88-F56301F5D0E4}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "starwars-api70", "ancillary-projects\starwars\starwars-api70\starwars-api70.csproj", "{B92A5C91-F88D-4F26-8775-20C72692BD43}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "graphql-aspnet-tests-common", "unit-tests\graphql-aspnet-tests-common\graphql-aspnet-tests-common.csproj", "{3CB086E3-5E7B-438B-9A95-AEA264009521}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "graphql-aspnet-testframework-tests", "unit-tests\graphql-aspnet-testframework-tests\graphql-aspnet-testframework-tests.csproj", "{E67D4FB2-73FF-4EC1-80F8-5D4DEC57F5AA}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "graphql-aspnet-tests-common", "unit-tests\graphql-aspnet-tests-common\graphql-aspnet-tests-common.csproj", "{3CB086E3-5E7B-438B-9A95-AEA264009521}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "starwars-api90", "ancillary-projects\starwars\starwars-api90\starwars-api90.csproj", "{E7EE5851-57F1-4E20-B4BA-06902C77E83A}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -60,10 +61,6 @@ Global {5DE081AA-494A-4377-B2CA-6952715D513D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5DE081AA-494A-4377-B2CA-6952715D513D}.Debug|Any CPU.Build.0 = Debug|Any CPU {5DE081AA-494A-4377-B2CA-6952715D513D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BAC0B33E-CA4D-4238-9E27-FDFC308440FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BAC0B33E-CA4D-4238-9E27-FDFC308440FC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BAC0B33E-CA4D-4238-9E27-FDFC308440FC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BAC0B33E-CA4D-4238-9E27-FDFC308440FC}.Release|Any CPU.Build.0 = Release|Any CPU {CA9D8DEC-13D6-4ED0-9CDC-747C2B5BCFB4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CA9D8DEC-13D6-4ED0-9CDC-747C2B5BCFB4}.Debug|Any CPU.Build.0 = Debug|Any CPU {CA9D8DEC-13D6-4ED0-9CDC-747C2B5BCFB4}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -71,14 +68,18 @@ Global {6E4A16F5-1B98-412E-9A88-F56301F5D0E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6E4A16F5-1B98-412E-9A88-F56301F5D0E4}.Debug|Any CPU.Build.0 = Debug|Any CPU {6E4A16F5-1B98-412E-9A88-F56301F5D0E4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B92A5C91-F88D-4F26-8775-20C72692BD43}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B92A5C91-F88D-4F26-8775-20C72692BD43}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B92A5C91-F88D-4F26-8775-20C72692BD43}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B92A5C91-F88D-4F26-8775-20C72692BD43}.Release|Any CPU.Build.0 = Release|Any CPU {3CB086E3-5E7B-438B-9A95-AEA264009521}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3CB086E3-5E7B-438B-9A95-AEA264009521}.Debug|Any CPU.Build.0 = Debug|Any CPU {3CB086E3-5E7B-438B-9A95-AEA264009521}.Release|Any CPU.ActiveCfg = Release|Any CPU {3CB086E3-5E7B-438B-9A95-AEA264009521}.Release|Any CPU.Build.0 = Release|Any CPU + {E67D4FB2-73FF-4EC1-80F8-5D4DEC57F5AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E67D4FB2-73FF-4EC1-80F8-5D4DEC57F5AA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E67D4FB2-73FF-4EC1-80F8-5D4DEC57F5AA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E67D4FB2-73FF-4EC1-80F8-5D4DEC57F5AA}.Release|Any CPU.Build.0 = Release|Any CPU + {E7EE5851-57F1-4E20-B4BA-06902C77E83A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E7EE5851-57F1-4E20-B4BA-06902C77E83A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E7EE5851-57F1-4E20-B4BA-06902C77E83A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E7EE5851-57F1-4E20-B4BA-06902C77E83A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -88,10 +89,10 @@ Global {88E1BE35-9BC9-43D3-A962-AAE247AE8430} = {350D3594-5D97-4D9B-A01D-D3A5C036318C} {5F6EBAF4-B5EB-4DBD-8F51-17BBC2E8984D} = {22C7BC5B-EC8E-4A07-9968-961E86AB44E2} {5DE081AA-494A-4377-B2CA-6952715D513D} = {350D3594-5D97-4D9B-A01D-D3A5C036318C} - {BAC0B33E-CA4D-4238-9E27-FDFC308440FC} = {22C7BC5B-EC8E-4A07-9968-961E86AB44E2} {6E4A16F5-1B98-412E-9A88-F56301F5D0E4} = {350D3594-5D97-4D9B-A01D-D3A5C036318C} - {B92A5C91-F88D-4F26-8775-20C72692BD43} = {22C7BC5B-EC8E-4A07-9968-961E86AB44E2} {3CB086E3-5E7B-438B-9A95-AEA264009521} = {350D3594-5D97-4D9B-A01D-D3A5C036318C} + {E67D4FB2-73FF-4EC1-80F8-5D4DEC57F5AA} = {350D3594-5D97-4D9B-A01D-D3A5C036318C} + {E7EE5851-57F1-4E20-B4BA-06902C77E83A} = {22C7BC5B-EC8E-4A07-9968-961E86AB44E2} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {DD2C38B8-B029-4DE1-877E-B1A661AC412F} diff --git a/src/graphql-aspnet.sln.DotSettings b/src/graphql-aspnet.sln.DotSettings index fd6abeb25..280d05ffe 100644 --- a/src/graphql-aspnet.sln.DotSettings +++ b/src/graphql-aspnet.sln.DotSettings @@ -246,7 +246,7 @@ graphql-aspnet repo: https://github.com/kevin-carroll/graphql-aspnet docs: -coming soon- -- -project: $PROJECT$ +project: ${File.ProjectName} -- License: MIT ************************************************************* @@ -265,6 +265,10 @@ License: MIT <Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB"><ExtraRule Prefix="ERROR_" Suffix="" Style="AA_BB" /></Policy> <Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /> <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb_AaBb" /> + <Policy><Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static readonly fields (private)"><ElementKinds><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Private" Description="Constant fields (private)"><ElementKinds><Kind Name="CONSTANT_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB"><ExtraRule Prefix="ERROR_" Suffix="" Style="AA_BB" /></Policy></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Protected, ProtectedInternal, Internal, Public, PrivateProtected" Description="Constant fields (not private)"><ElementKinds><Kind Name="CONSTANT_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB"><ExtraRule Prefix="ERROR_" Suffix="" Style="AA_BB" /></Policy></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Types and namespaces"><ElementKinds><Kind Name="NAMESPACE" /><Kind Name="CLASS" /><Kind Name="STRUCT" /><Kind Name="ENUM" /><Kind Name="DELEGATE" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb_AaBb" /></Policy> <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> @@ -337,6 +341,7 @@ License: MIT NOTIFY True + True True True True @@ -345,11 +350,13 @@ License: MIT True True True + True True True True True True + True True 3 diff --git a/src/graphql-aspnet/Attributes/GraphTypeAttribute.cs b/src/graphql-aspnet/Attributes/GraphTypeAttribute.cs index 1cba50fcf..05b4dab6f 100644 --- a/src/graphql-aspnet/Attributes/GraphTypeAttribute.cs +++ b/src/graphql-aspnet/Attributes/GraphTypeAttribute.cs @@ -19,7 +19,7 @@ namespace GraphQL.AspNet.Attributes /// item should be treated as a graph type and included in a schema. This attribute is /// optional depending on your schema configuration and naming preferences. /// - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Enum)] + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Enum | AttributeTargets.Struct)] public class GraphTypeAttribute : GraphAttributeBase { private TemplateDeclarationRequirements _templateDeclarationRequirements = TemplateDeclarationRequirements.None; diff --git a/src/graphql-aspnet/Attributes/TypeExtensionAttribute.cs b/src/graphql-aspnet/Attributes/TypeExtensionAttribute.cs index f7810c026..d8ebf96a7 100644 --- a/src/graphql-aspnet/Attributes/TypeExtensionAttribute.cs +++ b/src/graphql-aspnet/Attributes/TypeExtensionAttribute.cs @@ -16,7 +16,7 @@ namespace GraphQL.AspNet.Attributes using GraphQL.AspNet.Interfaces.Schema; /// - /// A decorator for a controller method declare it as an extension of another + /// A decorator for a controller method to declare it as an extension of another /// graph type (instead of as a query or mutation field). This attribute indicates that the /// method should be invoked in an individual format, being executed for each object /// being resolved. @@ -31,7 +31,7 @@ public class TypeExtensionAttribute : GraphFieldAttribute /// Initializes a new instance of the class. /// /// The concrete type to be extended. - /// Name of the field in the object graph (will be subjected to and altered according to schema naming rules). + /// Name of the field on the type being extended (will be subjected to and altered according to schema naming rules). public TypeExtensionAttribute(Type typeToExtend, string fieldName) : this(typeToExtend, fieldName, null) { @@ -41,7 +41,7 @@ public TypeExtensionAttribute(Type typeToExtend, string fieldName) /// Initializes a new instance of the class. /// /// The concrete type to be extended. - /// Name of the field in the object graph (will be subjected to and altered according to schema naming rules). + /// Name of the field on the type being extended (will be subjected to and altered according to schema naming rules). /// The type of the data object returned from this method. If this type implements /// this field will be declared as returning the union defined by the type. public TypeExtensionAttribute(Type typeToExtend, string fieldName, Type returnType) @@ -54,8 +54,8 @@ public TypeExtensionAttribute(Type typeToExtend, string fieldName, Type returnTy /// Initializes a new instance of the class. /// /// The concrete type to be extended. - /// Name of the field in the object graph (will be subjected to and altered according to schema naming rules). - /// Name of the union type to be created. + /// Name of the field on the type being extended (will be subjected to and altered according to schema naming rules). + /// Name of the union type this type extension will return. /// The first of two required types to include in the union. /// The second of two required types to include in the union. /// Any additional union types. diff --git a/src/graphql-aspnet/Common/Extensions/TypeExtensions.cs b/src/graphql-aspnet/Common/Extensions/TypeExtensions.cs index d3b24852b..165d0be32 100644 --- a/src/graphql-aspnet/Common/Extensions/TypeExtensions.cs +++ b/src/graphql-aspnet/Common/Extensions/TypeExtensions.cs @@ -15,6 +15,7 @@ namespace GraphQL.AspNet.Common.Extensions using System.ComponentModel.DataAnnotations; using System.Linq; using System.Reflection; + using GraphQL.AspNet.Schemas.TypeSystem.Introspection; /// /// Extension methods for working with .NET @@ -506,5 +507,19 @@ public static bool IsStruct(this Type type) return type.IsValueType && !type.IsPrimitive && !type.IsEnum && !type.IsArray && !type.IsInterface && !type.IsAbstract; } + + /// + /// Determines whether the specified type represents a record declared in + /// source code. + /// + /// The type to inspect. + /// true if the specified type is record; otherwise, false. + public static bool IsRecord(this Type type) + { + if (type == null) + return false; + + return type.GetMethods().Any(m => m.Name == "$"); + } } } \ No newline at end of file diff --git a/src/graphql-aspnet/Common/TimerAsync.cs b/src/graphql-aspnet/Common/TimerAsync.cs index 6fd016c72..2cf02f354 100644 --- a/src/graphql-aspnet/Common/TimerAsync.cs +++ b/src/graphql-aspnet/Common/TimerAsync.cs @@ -90,7 +90,8 @@ public void Start() } finally { - _semaphore.Release(); + if (!_disposed) + _semaphore.Release(); } } @@ -121,7 +122,8 @@ public async Task StopAsync() finally { this.IsRunning = false; - _semaphore.Release(); + if (!_disposed) + _semaphore.Release(); } } @@ -195,4 +197,4 @@ public void Dispose() /// true if the timer is running; otherwise, false. public bool IsRunning { get; private set; } } -} +} \ No newline at end of file diff --git a/src/graphql-aspnet/Configuration/GraphQLSchemaBuilderExtensions.cs b/src/graphql-aspnet/Configuration/GraphQLSchemaBuilderExtensions.cs index b4064ec0f..9f852f978 100644 --- a/src/graphql-aspnet/Configuration/GraphQLSchemaBuilderExtensions.cs +++ b/src/graphql-aspnet/Configuration/GraphQLSchemaBuilderExtensions.cs @@ -28,23 +28,50 @@ namespace GraphQL.AspNet.Configuration /// public static class GraphQLSchemaBuilderExtensions { - private static readonly Dictionary SCHEMA_REGISTRATIONS; + private static readonly Dictionary SCHEMA_REGISTRATIONS; /// /// Initializes static members of the class. /// static GraphQLSchemaBuilderExtensions() { - SCHEMA_REGISTRATIONS = new Dictionary(); + SCHEMA_REGISTRATIONS = new Dictionary(); } /// /// Helper method to null out the schema registration references. Useful in testing and after setup is complete there is no /// need to keep the reference chain in tact. /// + /// The service collection to clear. + public static void Clear(IServiceCollection serviceCollection) + { + lock (SCHEMA_REGISTRATIONS) + { + if (!SCHEMA_REGISTRATIONS.TryGetValue(serviceCollection, out var value)) + { + return; + } + + value.Clear(); + SCHEMA_REGISTRATIONS.Remove(serviceCollection); + } + } + + /// + /// Helper method to null out all schema injector collections. Useful in testing and after setup is complete there is no + /// need to keep the reference chain in tact. + /// public static void Clear() { - SCHEMA_REGISTRATIONS.Clear(); + lock (SCHEMA_REGISTRATIONS) + { + foreach (var injectorCollection in SCHEMA_REGISTRATIONS.Values) + { + injectorCollection.Clear(); + } + + SCHEMA_REGISTRATIONS.Clear(); + } } /// @@ -75,7 +102,8 @@ public static ISchemaBuilder AddGraphQL( where TSchema : class, ISchema { Validation.ThrowIfNull(serviceCollection, nameof(serviceCollection)); - if (SCHEMA_REGISTRATIONS.ContainsKey(typeof(TSchema))) + var injectorCollection = GetOrAddSchemaInjectorCollection(serviceCollection); + if (injectorCollection.ContainsKey(typeof(TSchema))) { throw new GraphTypeDeclarationException( $"The schema type {typeof(TSchema).FriendlyName()} has already been registered. " + @@ -84,7 +112,7 @@ public static ISchemaBuilder AddGraphQL( var schemaOptions = new SchemaOptions(serviceCollection); var injector = new GraphQLSchemaInjector(schemaOptions, options); - SCHEMA_REGISTRATIONS.Add(typeof(TSchema), injector); + injectorCollection.Add(typeof(TSchema), injector); injector.ConfigureServices(); return injector.SchemaBuilder; @@ -111,12 +139,13 @@ public static ISchemaBuilder AddGraphQL( /// The application being constructed. public static void UseGraphQL(this IApplicationBuilder app) { - foreach (var injector in SCHEMA_REGISTRATIONS.Values) + var injectorCollection = app.ApplicationServices.GetRequiredService(); + foreach (var injector in injectorCollection.Values) { injector.UseSchema(app); } - Clear(); + Clear(injectorCollection.ServiceCollection); } /// @@ -133,12 +162,38 @@ public static void UseGraphQL(this IApplicationBuilder app) /// graphql runtime. public static void UseGraphQL(this IServiceProvider serviceProvider) { - foreach (var injector in SCHEMA_REGISTRATIONS.Values) + var injectorCollection = serviceProvider.GetRequiredService(); + foreach (var injector in injectorCollection.Values) { injector.UseSchema(serviceProvider); } - Clear(); + Clear(injectorCollection.ServiceCollection); + } + + /// + /// Get or create a schema injector collection for the service collection being harnessed. + /// + /// The service collection to create schema injector collection for + /// Existing or new schema injector collection + private static ISchemaInjectorCollection GetOrAddSchemaInjectorCollection(IServiceCollection serviceCollection) + { + if (SCHEMA_REGISTRATIONS.TryGetValue(serviceCollection, out var value)) + return value; + + lock (SCHEMA_REGISTRATIONS) + { + if (SCHEMA_REGISTRATIONS.TryGetValue(serviceCollection, out value)) + return value; + + var injectorCollection = new SchemaInjectorCollection(serviceCollection); + + serviceCollection.AddSingleton(injectorCollection); + value = injectorCollection; + SCHEMA_REGISTRATIONS.Add(serviceCollection, value); + } + + return value; } } } \ No newline at end of file diff --git a/src/graphql-aspnet/Configuration/ResolverIsolationOptionsExtensions.cs b/src/graphql-aspnet/Configuration/ResolverIsolationOptionsExtensions.cs index c784645e0..51dc59cf2 100644 --- a/src/graphql-aspnet/Configuration/ResolverIsolationOptionsExtensions.cs +++ b/src/graphql-aspnet/Configuration/ResolverIsolationOptionsExtensions.cs @@ -9,6 +9,7 @@ namespace GraphQL.AspNet.Configuration { + using System; using GraphQL.AspNet.Internal.TypeTemplates; /// diff --git a/src/graphql-aspnet/Configuration/SchemaExecutionConfiguration.cs b/src/graphql-aspnet/Configuration/SchemaExecutionConfiguration.cs index 7bf5e61f1..3320b0294 100644 --- a/src/graphql-aspnet/Configuration/SchemaExecutionConfiguration.cs +++ b/src/graphql-aspnet/Configuration/SchemaExecutionConfiguration.cs @@ -72,6 +72,4 @@ public ResolverIsolationOptions ResolverIsolation /// public bool DebugMode { get; set; } } - -#pragma warning restore CS0618 // Type or member is obsolete } \ No newline at end of file diff --git a/src/graphql-aspnet/Configuration/SchemaInjectorCollection.cs b/src/graphql-aspnet/Configuration/SchemaInjectorCollection.cs new file mode 100644 index 000000000..3f45f1a13 --- /dev/null +++ b/src/graphql-aspnet/Configuration/SchemaInjectorCollection.cs @@ -0,0 +1,35 @@ +// ************************************************************* +// project: graphql-aspnet +// -- +// repo: https://github.com/graphql-aspnet +// docs: https://graphql-aspnet.github.io +// -- +// License: MIT +// ************************************************************* + +namespace GraphQL.AspNet.Configuration +{ + using System; + using System.Collections.Generic; + using GraphQL.AspNet.Common; + using GraphQL.AspNet.Interfaces.Configuration; + using Microsoft.Extensions.DependencyInjection; + + /// + /// A schema injector collection implementation. + /// + public class SchemaInjectorCollection : Dictionary, ISchemaInjectorCollection + { + /// + /// Initializes a new instance of the class. + /// + /// The service collection this set is tracking against. + public SchemaInjectorCollection(IServiceCollection serviceCollection) + { + this.ServiceCollection = Validation.ThrowIfNullOrReturn(serviceCollection, nameof(serviceCollection)); + } + + /// + public IServiceCollection ServiceCollection { get; } + } +} \ No newline at end of file diff --git a/src/graphql-aspnet/Configuration/Startup/GraphQLSchemaInjector{TSchema}.cs b/src/graphql-aspnet/Configuration/Startup/GraphQLSchemaInjector{TSchema}.cs index beb1f2c56..2814aae97 100644 --- a/src/graphql-aspnet/Configuration/Startup/GraphQLSchemaInjector{TSchema}.cs +++ b/src/graphql-aspnet/Configuration/Startup/GraphQLSchemaInjector{TSchema}.cs @@ -162,6 +162,7 @@ private void RegisterEngineComponents() _options.ServiceCollection.TryAddTransient(typeof(IGraphQLHttpProcessor), _options.QueryHandler.HttpProcessorType); // "per application server" instance + _options.ServiceCollection.TryAddScoped(); _options.ServiceCollection.TryAddScoped(sp => sp?.GetService()); _options.ServiceCollection.TryAddScoped((sp) => { diff --git a/src/graphql-aspnet/Constants.cs b/src/graphql-aspnet/Constants.cs index 634b885c6..6c63e5e0f 100644 --- a/src/graphql-aspnet/Constants.cs +++ b/src/graphql-aspnet/Constants.cs @@ -108,14 +108,25 @@ public static class Messaging /// The known ignored field names. public static ISet IgnoredFieldNames { get; } = new HashSet() { - // object declared methods that might be exposed - // also methods internally declared by structs that might be exposed + // object methods that might be exposed + // also methods internally declared by structs or records that might be exposed "Deconstruct", nameof(object.ToString), nameof(object.GetHashCode), nameof(object.GetType), }; + /// + /// Gets a set of field names (property or method) that are never parsed by the templating engine + /// for any reason when parsing a record. + /// + /// The known ignored field names. + public static ISet IgnoredRecordFieldNames { get; } = new HashSet() + { + "Equals", + "$", + }; + /// /// A collection of common suffixes that are semantically handled or removed from naming. /// diff --git a/src/graphql-aspnet/Controllers/GraphController_ActionResults.cs b/src/graphql-aspnet/Controllers/GraphController_ActionResults.cs index 70aeaf043..4a31e0539 100644 --- a/src/graphql-aspnet/Controllers/GraphController_ActionResults.cs +++ b/src/graphql-aspnet/Controllers/GraphController_ActionResults.cs @@ -102,7 +102,7 @@ protected virtual IGraphActionResult InternalServerError(string errorMessage) /// /// The message indicating what was not found. /// IGraphActionResult. - protected virtual IGraphActionResult NotFound(string message) + protected virtual IGraphActionResult NotFound(string message = null) { return new RouteNotFoundGraphActionResult(message); } diff --git a/src/graphql-aspnet/Engine/DefaultGraphQLHttpProcessor{TSchema}.cs b/src/graphql-aspnet/Engine/DefaultGraphQLHttpProcessor{TSchema}.cs index d438eeb85..b278e43eb 100644 --- a/src/graphql-aspnet/Engine/DefaultGraphQLHttpProcessor{TSchema}.cs +++ b/src/graphql-aspnet/Engine/DefaultGraphQLHttpProcessor{TSchema}.cs @@ -214,7 +214,7 @@ protected virtual async Task WriteResponseAsync(IQueryExecutionResult result, Ca this.Response.ContentType = Constants.MediaTypes.JSON; if (this.Schema.Configuration.ResponseOptions.AppendServerHeader) { - this.Response.Headers.Add(Constants.ServerInformation.SERVER_INFORMATION_HEADER, Constants.ServerInformation.ServerData); + this.Response.Headers.Append(Constants.ServerInformation.SERVER_INFORMATION_HEADER, Constants.ServerInformation.ServerData); } var localWriter = new GraphQLHttpResponseWriter( diff --git a/src/graphql-aspnet/Execution/Contexts/QueryExecutionContext.cs b/src/graphql-aspnet/Execution/Contexts/QueryExecutionContext.cs index 48675d99d..26042c4c5 100644 --- a/src/graphql-aspnet/Execution/Contexts/QueryExecutionContext.cs +++ b/src/graphql-aspnet/Execution/Contexts/QueryExecutionContext.cs @@ -12,6 +12,7 @@ namespace GraphQL.AspNet.Execution.Contexts using System; using System.Collections.Generic; using System.Diagnostics; + using System.Threading; using GraphQL.AspNet.Execution; using GraphQL.AspNet.Execution.FieldResolution; using GraphQL.AspNet.Interfaces.Execution; diff --git a/src/graphql-aspnet/Execution/Contexts/SortedFieldExecutionContextList.cs b/src/graphql-aspnet/Execution/Contexts/SortedFieldExecutionContextList.cs deleted file mode 100644 index 6d3a8471d..000000000 --- a/src/graphql-aspnet/Execution/Contexts/SortedFieldExecutionContextList.cs +++ /dev/null @@ -1,62 +0,0 @@ -// ************************************************************* -// project: graphql-aspnet -// -- -// repo: https://github.com/graphql-aspnet -// docs: https://graphql-aspnet.github.io -// -- -// License: MIT -// ************************************************************* - -namespace GraphQL.AspNet.Execution.Contexts -{ - using System.Collections.Generic; - using GraphQL.AspNet.Common; - using GraphQL.AspNet.Interfaces.Execution; - - /// - /// A customized queue for ordering - /// for execution. - /// - public class SortedFieldExecutionContextList - { - private List _syncList; - private List _paralellList; - - /// - /// Initializes a new instance of the class. - /// - public SortedFieldExecutionContextList() - { - _syncList = new List(); - _paralellList = new List(); - } - - /// - /// Adds a new execution context to the collection. - /// - /// The context to insert. - /// if set to true the context will be flagged - /// as needing to be run in isolation, not in parralel with others. - public void Add(GraphFieldExecutionContext context, bool executeInIsolation) - { - Validation.ThrowIfNull(context, nameof(context)); - - if (executeInIsolation) - _syncList.Add(context); - else - _paralellList.Add(context); - } - - /// - /// Gets the contexts of this set that must be executed in isolation. - /// - /// The isolated contexts. - public IEnumerable IsolatedContexts => _syncList; - - /// - /// Gets the contexts of this set that can be executed simultaniously. - /// - /// The paralell contexts. - public IEnumerable ParalellContexts => _paralellList; - } -} \ No newline at end of file diff --git a/src/graphql-aspnet/Execution/DirectiveProcessorTypeSystem.cs b/src/graphql-aspnet/Execution/DirectiveProcessorTypeSystem.cs index 349c7b488..ef745eb78 100644 --- a/src/graphql-aspnet/Execution/DirectiveProcessorTypeSystem.cs +++ b/src/graphql-aspnet/Execution/DirectiveProcessorTypeSystem.cs @@ -15,12 +15,12 @@ namespace GraphQL.AspNet.Execution using System.Threading; using GraphQL.AspNet.Common; using GraphQL.AspNet.Common.Extensions; - using GraphQL.AspNet.Execution.Source; using GraphQL.AspNet.Configuration.Exceptions; using GraphQL.AspNet.Directives; using GraphQL.AspNet.Execution.Contexts; using GraphQL.AspNet.Execution.Exceptions; using GraphQL.AspNet.Execution.QueryPlans.InputArguments; + using GraphQL.AspNet.Execution.Source; using GraphQL.AspNet.Interfaces.Execution; using GraphQL.AspNet.Interfaces.Execution.QueryPlans.InputArguments; using GraphQL.AspNet.Interfaces.Logging; diff --git a/src/graphql-aspnet/Execution/GraphExecutionMessage.cs b/src/graphql-aspnet/Execution/GraphExecutionMessage.cs index 92773d8fa..2b89b1bba 100644 --- a/src/graphql-aspnet/Execution/GraphExecutionMessage.cs +++ b/src/graphql-aspnet/Execution/GraphExecutionMessage.cs @@ -13,8 +13,8 @@ namespace GraphQL.AspNet.Execution using System.Collections.Generic; using System.Diagnostics; using GraphQL.AspNet.Execution.Source; - using GraphQL.AspNet.Interfaces.Execution.RulesEngine; using GraphQL.AspNet.Interfaces.Execution; + using GraphQL.AspNet.Interfaces.Execution.RulesEngine; /// /// A default, concrete implementation of a used @@ -76,6 +76,9 @@ internal static IGraphMessage FromValidationRule( return graphMessage; } + private string _message; + private string _code; + /// /// Initializes a new instance of the class. /// @@ -92,58 +95,41 @@ public GraphExecutionMessage( Exception exception = null) { this.Origin = origin; - this.Code = code?.Trim() ?? "-unknown-"; - this.Message = message?.Trim(); + this.Code = code; + this.Message = message; this.Severity = severity; this.Exception = exception; this.TimeStamp = DateTimeOffset.UtcNow; this.MetaData = new Dictionary(); } - /// - /// Gets the time stamp when this message was created. - /// - /// The time stamp. + /// public DateTimeOffset TimeStamp { get; } - /// - /// Gets the origin in the provided source text, if any, this message relates to. - /// This value is returned as part of a query response. - /// - /// The location. + /// public SourceOrigin Origin { get; } - /// - /// Gets an error code identifying this error. This value is returned as part of a query response. - /// - /// The code. - public string Code { get; } + /// + public string Code + { + get => _code; + set => _code = value?.Trim() ?? Constants.ErrorCodes.DEFAULT; + } - /// - /// Gets a human-friendly message that conveys details about the error tht occured. This value is - /// returned as part of a query response. - /// - /// The message. - public string Message { get; } + /// + public string Message + { + get => _message; + set => _message = value?.Trim(); + } - /// - /// Gets an (optional) exception that may have occured to generate the error. The exception - /// is only conveyed to the requestor if the request is configured to expose exceptions. - /// - /// The exception. - public Exception Exception { get; } + /// + public Exception Exception { get; set; } - /// - /// Gets the severity of this message that was generated. - /// - /// The severity. + /// public GraphMessageSeverity Severity { get; } - /// - /// Gets additional metadata defined for this message. This data will be added as key/value pairs - /// when the message is rendered to an graph output. - /// - /// The meta data. + /// public IDictionary MetaData { get; } } } \ No newline at end of file diff --git a/src/graphql-aspnet/Execution/GraphMessageCollection.cs b/src/graphql-aspnet/Execution/GraphMessageCollection.cs index 844f747f2..6632dbc16 100644 --- a/src/graphql-aspnet/Execution/GraphMessageCollection.cs +++ b/src/graphql-aspnet/Execution/GraphMessageCollection.cs @@ -16,11 +16,7 @@ namespace GraphQL.AspNet.Execution using GraphQL.AspNet.Execution.Source; using GraphQL.AspNet.Interfaces.Execution; - /// - /// A collection of messages produced while completing a requested graph operation. Messages generated - /// by the runtime or by custom code on field requests are aggregated and inspected for severity levels to - /// deteremine if processing should cease or when a response needs to be sent to the requestor. - /// + /// [DebuggerDisplay("Count = {Count}, Severity = {Severity}")] [DebuggerTypeProxy(typeof(GraphMessageCollectionDebugProxy))] [DebuggerStepThrough] diff --git a/src/graphql-aspnet/Execution/GraphQLFieldResolverIsolationManager.cs b/src/graphql-aspnet/Execution/GraphQLFieldResolverIsolationManager.cs new file mode 100644 index 000000000..62202abe1 --- /dev/null +++ b/src/graphql-aspnet/Execution/GraphQLFieldResolverIsolationManager.cs @@ -0,0 +1,87 @@ +// ************************************************************* +// project: graphql-aspnet +// -- +// repo: https://github.com/graphql-aspnet +// docs: https://graphql-aspnet.github.io +// -- +// License: MIT +// ************************************************************* + +namespace GraphQL.AspNet.Execution +{ + using System; + using System.Threading; + using System.Threading.Tasks; + using GraphQL.AspNet.Common; + using GraphQL.AspNet.Configuration; + using GraphQL.AspNet.Interfaces.Execution; + using GraphQL.AspNet.Interfaces.Schema; + using GraphQL.AspNet.Internal.TypeTemplates; + + /// + /// A default implementation of the resolver isolation manager, wrapping a simple + /// . + /// + public class GraphQLFieldResolverIsolationManager : IGraphQLFieldResolverIsolationManager, IDisposable + { + private readonly SemaphoreSlim _semaphore; + private bool _disposedValue; + + /// + /// Initializes a new instance of the class. + /// + public GraphQLFieldResolverIsolationManager() + { + _semaphore = new SemaphoreSlim(1); + } + + /// + public async Task WaitAsync() + { + await _semaphore.WaitAsync(); + } + + /// + public void Release() + { + _semaphore.Release(); + } + + /// + public bool ShouldIsolate(ISchema schema, GraphFieldSource fieldSource) + { + Validation.ThrowIfNull(schema, nameof(schema)); + + return schema.Configuration.ExecutionOptions.DebugMode || + schema.Configuration + .ExecutionOptions + .ResolverIsolation + .ShouldIsolateFieldSource(fieldSource); + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + _semaphore.Dispose(); + } + + _disposedValue = true; + } + } + + /// + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} \ No newline at end of file diff --git a/src/graphql-aspnet/Execution/QueryPlans/DocumentParts/DocumentNamedFragmentCollection.cs b/src/graphql-aspnet/Execution/QueryPlans/DocumentParts/DocumentNamedFragmentCollection.cs index 45adcbb81..406a5a942 100644 --- a/src/graphql-aspnet/Execution/QueryPlans/DocumentParts/DocumentNamedFragmentCollection.cs +++ b/src/graphql-aspnet/Execution/QueryPlans/DocumentParts/DocumentNamedFragmentCollection.cs @@ -13,16 +13,17 @@ namespace GraphQL.AspNet.Execution.QueryPlans.DocumentParts using System.Collections.Generic; using System.Diagnostics; using GraphQL.AspNet.Common; - using GraphQL.AspNet.Common.Generics; using GraphQL.AspNet.Interfaces.Execution.QueryPlans.DocumentParts; + using OrderedDictionaryOfStringAndNamedFragment = GraphQL.AspNet.Common.Generics.OrderedDictionary; + /// /// An indexed collection of named fragments contained within a query document. /// [DebuggerDisplay("Count = {Count}")] internal class DocumentNamedFragmentCollection : INamedFragmentCollectionDocumentPart { - private OrderedDictionary _fragments; + private OrderedDictionaryOfStringAndNamedFragment _fragments; // its possible that a document declares more than one named fragment with the same // name. Keep track of the duplicates for document validation reporting. @@ -35,7 +36,7 @@ internal class DocumentNamedFragmentCollection : INamedFragmentCollectionDocumen public DocumentNamedFragmentCollection(IDocumentPart parent) { this.Parent = Validation.ThrowIfNullOrReturn(parent, nameof(parent)); - _fragments = new OrderedDictionary(); + _fragments = new OrderedDictionaryOfStringAndNamedFragment(); } /// diff --git a/src/graphql-aspnet/Execution/Response/ResponseFieldSet.cs b/src/graphql-aspnet/Execution/Response/ResponseFieldSet.cs index 1170099d0..208976e2a 100644 --- a/src/graphql-aspnet/Execution/Response/ResponseFieldSet.cs +++ b/src/graphql-aspnet/Execution/Response/ResponseFieldSet.cs @@ -11,23 +11,24 @@ namespace GraphQL.AspNet.Execution.Response { using System.Collections.Generic; using System.Diagnostics; - using GraphQL.AspNet.Common.Generics; using GraphQL.AspNet.Interfaces.Execution.Response; + using OrderedDictionaryOfStringAndQueryResponseItem = GraphQL.AspNet.Common.Generics.OrderedDictionary; + /// /// A collection of keyed items included as a result to a graphql query. /// [DebuggerDisplay("Count = {Fields.Count}")] internal class ResponseFieldSet : IQueryResponseFieldSet { - private readonly OrderedDictionary _dictionary; + private readonly OrderedDictionaryOfStringAndQueryResponseItem _dictionary; /// /// Initializes a new instance of the class. /// public ResponseFieldSet() { - _dictionary = new OrderedDictionary(); + _dictionary = new OrderedDictionaryOfStringAndQueryResponseItem(); } /// diff --git a/src/graphql-aspnet/Execution/RulesEngine/RuleSets/DocumentValidation/FieldSelectionSteps/Rule_5_3_2_FieldsOfIdenticalOutputMustHaveIdenticalSigs.cs b/src/graphql-aspnet/Execution/RulesEngine/RuleSets/DocumentValidation/FieldSelectionSteps/Rule_5_3_2_FieldsOfIdenticalOutputMustHaveIdenticalSigs.cs index 0fe589b58..c62145648 100644 --- a/src/graphql-aspnet/Execution/RulesEngine/RuleSets/DocumentValidation/FieldSelectionSteps/Rule_5_3_2_FieldsOfIdenticalOutputMustHaveIdenticalSigs.cs +++ b/src/graphql-aspnet/Execution/RulesEngine/RuleSets/DocumentValidation/FieldSelectionSteps/Rule_5_3_2_FieldsOfIdenticalOutputMustHaveIdenticalSigs.cs @@ -76,10 +76,22 @@ private bool CompareAllFields( if (fields.Count < 2) return true; + // must compare every field to every other field + // inside the field set O(n^2) time :( + // take a short cut if some fields are not in a state + // that warrants complete validation for (var i = 0; i < fields.Count - 1; i++) { + if (!this.ShouldBeValidatedForRule(fields[i])) + continue; + for (var j = i + 1; j < fields.Count; j++) { + // don't attempt to perform a validation unless both + // fields are correct and validatable + if (!this.ShouldBeValidatedForRule(fields[j])) + continue; + var passedValidation = this.ValidateFieldPair( context, ownerField, @@ -226,6 +238,21 @@ private bool AreSameShape(IFieldDocumentPart leftField, IFieldDocumentPart right return true; } + /// + /// Determines whether the field document part should be validated against this rule. Field + /// document parts may not qualify for validation if other errors may exist within them. + /// + /// The field to check. + /// true if the document parts can be checked for co-existance; otherwise, false. + private bool ShouldBeValidatedForRule(IFieldDocumentPart fieldToCheck) + { + IGraphType graphType = null; + if (fieldToCheck.Parent is IFieldSelectionSetDocumentPart fsdl) + graphType = fsdl.GraphType; + + return graphType != null; + } + /// /// Inspects both fields to see if any target graph type restrctions exist (such as with fragmetn spreads) /// such that the fields could not be included in the same object resolution. For example if the existing field targets a @@ -246,7 +273,7 @@ private bool CanCoExist(ISchema targetSchema, IFieldDocumentPart leftField, IFie if (rightField.Parent is IFieldSelectionSetDocumentPart fsdr) rightSourceGraphType = fsdr.GraphType; - // neither should be null at this point + // last ditch safety check: neither should be null at this point. if (leftSourceGraphType == null) { throw new GraphExecutionException( diff --git a/src/graphql-aspnet/Execution/RulesEngine/RuleSets/DocumentValidation/QueryFragmentSteps/Rule_5_5_1_2_InlineFragmentGraphTypeMustExistInTheSchema.cs b/src/graphql-aspnet/Execution/RulesEngine/RuleSets/DocumentValidation/QueryFragmentSteps/Rule_5_5_1_2_InlineFragmentGraphTypeMustExistInTheSchema.cs index b36c4e78a..f327d19e0 100644 --- a/src/graphql-aspnet/Execution/RulesEngine/RuleSets/DocumentValidation/QueryFragmentSteps/Rule_5_5_1_2_InlineFragmentGraphTypeMustExistInTheSchema.cs +++ b/src/graphql-aspnet/Execution/RulesEngine/RuleSets/DocumentValidation/QueryFragmentSteps/Rule_5_5_1_2_InlineFragmentGraphTypeMustExistInTheSchema.cs @@ -28,7 +28,7 @@ public override bool Execute(DocumentValidationContext context) { this.ValidationError( context, - $"No known graph type was found for the target fragment."); + $"No known graph type was found for the target fragment (Target: '{fragment.TargetGraphTypeName}')."); return false; } diff --git a/src/graphql-aspnet/Interfaces/Configuration/ISchemaInjectorCollection.cs b/src/graphql-aspnet/Interfaces/Configuration/ISchemaInjectorCollection.cs new file mode 100644 index 000000000..17cdc0802 --- /dev/null +++ b/src/graphql-aspnet/Interfaces/Configuration/ISchemaInjectorCollection.cs @@ -0,0 +1,28 @@ +// ************************************************************* +// project: graphql-aspnet +// -- +// repo: https://github.com/graphql-aspnet +// docs: https://graphql-aspnet.github.io +// -- +// License: MIT +// ************************************************************* + +namespace GraphQL.AspNet.Interfaces.Configuration +{ + using System; + using System.Collections.Generic; + using Microsoft.Extensions.DependencyInjection; + + /// + /// An interface used by the injector to track multiple schema injectors + /// used for injecting. + /// + public interface ISchemaInjectorCollection : IDictionary + { + /// + /// Gets the service collection to which this collection is attached. + /// + /// The attached service collection. + public IServiceCollection ServiceCollection { get; } + } +} \ No newline at end of file diff --git a/src/graphql-aspnet/Interfaces/Execution/IGraphMessage.cs b/src/graphql-aspnet/Interfaces/Execution/IGraphMessage.cs index f0eb91f68..6bfa95069 100644 --- a/src/graphql-aspnet/Interfaces/Execution/IGraphMessage.cs +++ b/src/graphql-aspnet/Interfaces/Execution/IGraphMessage.cs @@ -11,8 +11,8 @@ namespace GraphQL.AspNet.Interfaces.Execution { using System; using System.Collections.Generic; - using GraphQL.AspNet.Execution.Source; using GraphQL.AspNet.Execution; + using GraphQL.AspNet.Execution.Source; /// /// An error of some kind that occured during the resolution of any graph item. @@ -22,6 +22,9 @@ public interface IGraphMessage /// /// Gets the time stamp (in UTC-0) when this message was created. /// + /// + /// This value is immutable once the message is created. + /// /// The time stamp. DateTimeOffset TimeStamp { get; } @@ -29,32 +32,38 @@ public interface IGraphMessage /// Gets the origin in the provided source text, if any, this message relates to. /// This value is returned as part of a query response. /// + /// + /// This value is immutable once the message is created. + /// /// The location. SourceOrigin Origin { get; } /// - /// Gets an error code identifying this error. This value is returned as part of a query response. + /// Gets or sets a standardized error code identifying this error. This value is returned as part of a query response. /// /// The code that can idenify this message or message type to a user. - string Code { get; } + string Code { get; set; } /// - /// Gets a human-friendly message that conveys details about the error tht occured. This value is + /// Gets or sets a human-friendly message that conveys details about the error tht occured. This value is /// returned as part of a query response. /// - /// The message. - string Message { get; } + /// The message contents. + string Message { get; set; } /// - /// Gets an (optional) exception that may have occured to generate the error. The exception + /// Gets or sets an (optional) exception that may have occured to generate the error. The exception /// is only conveyed to the requestor if the request is configured to expose exceptions. /// /// The exception that was originally thrown. - Exception Exception { get; } + Exception Exception { get; set; } /// /// Gets the severity of this message that was generated. /// + /// + /// This value is immutable once the message is created. + /// /// The severity of the message. GraphMessageSeverity Severity { get; } diff --git a/src/graphql-aspnet/Interfaces/Execution/IGraphMessageCollection.cs b/src/graphql-aspnet/Interfaces/Execution/IGraphMessageCollection.cs index 4c48f855e..fd21c1e1c 100644 --- a/src/graphql-aspnet/Interfaces/Execution/IGraphMessageCollection.cs +++ b/src/graphql-aspnet/Interfaces/Execution/IGraphMessageCollection.cs @@ -11,14 +11,18 @@ namespace GraphQL.AspNet.Interfaces.Execution { using System; using System.Collections.Generic; - using GraphQL.AspNet.Execution.Source; using GraphQL.AspNet.Execution; + using GraphQL.AspNet.Execution.Source; /// /// A collection of messages produced while completing a requested graph operation. Messages generated /// by the runtime or by custom code on field requests are aggregated and inspected for severity levels to /// deteremine if processing should cease or when a response needs to be sent to the request. /// + /// + /// Once added, the core identity of a message (e.g. , etc.) cannot be changed. + /// However, user facing values such as the or can be updated at will. + /// public interface IGraphMessageCollection : IReadOnlyList { /// diff --git a/src/graphql-aspnet/Interfaces/Execution/IGraphQLFieldResolverIsolationManager.cs b/src/graphql-aspnet/Interfaces/Execution/IGraphQLFieldResolverIsolationManager.cs new file mode 100644 index 000000000..2cb96633b --- /dev/null +++ b/src/graphql-aspnet/Interfaces/Execution/IGraphQLFieldResolverIsolationManager.cs @@ -0,0 +1,46 @@ +// ************************************************************* +// project: graphql-aspnet +// -- +// repo: https://github.com/graphql-aspnet +// docs: https://graphql-aspnet.github.io +// -- +// License: MIT +// ************************************************************* + +namespace GraphQL.AspNet.Interfaces.Execution +{ + using System.Threading; + using System.Threading.Tasks; + using GraphQL.AspNet.Interfaces.Schema; + using GraphQL.AspNet.Internal.TypeTemplates; + + /// + /// A manager that will allow GraphQL field resolvers to take isolated control + /// and be the only field "resolving" at a given point in time. + /// + /// The default implementation is a simple wrapper on a . + public interface IGraphQLFieldResolverIsolationManager + { + /// + /// Determines if the given field should execute in a isolated mode on the target + /// schema. + /// + /// The schema governing the request. + /// The source of the field or directive being checked. + /// true if the field should execute in an isolated mode, false otherwise. + bool ShouldIsolate(ISchema schema, GraphFieldSource fieldSource); + + /// + /// A call that will complete when the current request has been granted control + /// by the isolation manager. + /// + /// Task. + Task WaitAsync(); + + /// + /// Releases the caller's hold on the isolation context, allowing another + /// field to take control if necessary. + /// + void Release(); + } +} \ No newline at end of file diff --git a/src/graphql-aspnet/Internal/Resolvers/GraphControllerActionResolver.cs b/src/graphql-aspnet/Internal/Resolvers/GraphControllerActionResolver.cs index 028ae10d1..9fac1eb0e 100644 --- a/src/graphql-aspnet/Internal/Resolvers/GraphControllerActionResolver.cs +++ b/src/graphql-aspnet/Internal/Resolvers/GraphControllerActionResolver.cs @@ -17,8 +17,10 @@ namespace GraphQL.AspNet.Internal.Resolvers using GraphQL.AspNet.Controllers; using GraphQL.AspNet.Controllers.ActionResults; using GraphQL.AspNet.Execution.Contexts; + using GraphQL.AspNet.Execution.Exceptions; using GraphQL.AspNet.Interfaces.Controllers; using GraphQL.AspNet.Interfaces.Execution; + using Microsoft.Extensions.DependencyInjection; /// /// A special resolver specifically for actions on a . Provides extra @@ -42,6 +44,9 @@ public GraphControllerActionResolver(IGraphFieldResolverMethod actionMethod) public async Task ResolveAsync(FieldResolutionContext context, CancellationToken cancelToken = default) { IGraphActionResult result; + var isolationObtained = false; + IGraphQLFieldResolverIsolationManager isolationManager = null; + try { // create a scoped controller instance for this invocation @@ -49,14 +54,31 @@ public async Task ResolveAsync(FieldResolutionContext context, CancellationToken .ServiceProvider? .GetService(_actionMethod.Parent.ObjectType) as GraphController; + isolationManager = context + .ServiceProvider? + .GetService(); + if (controller == null) { result = new RouteNotFoundGraphActionResult( $"The controller assigned to process the field '{context.Request.InvocationContext.Field.Route.Path}' " + "was not found."); } + else if (isolationManager == null) + { + throw new GraphExecutionException( + $"No {nameof(IGraphQLFieldResolverIsolationManager)} was configured for the request. " + + $"Unable to determine the isolation requirements for the resolver of field '{context.Request.InvocationContext.Field.Route.Path}'"); + } else { + var shouldIsolate = isolationManager.ShouldIsolate(context.Schema, context.Request.Field.FieldSource); + if (shouldIsolate) + { + await isolationManager.WaitAsync(); + isolationObtained = true; + } + // invoke the right action method and set a result. var task = controller.InvokeActionAsync(_actionMethod, context); var returnedItem = await task.ConfigureAwait(false); @@ -68,6 +90,11 @@ public async Task ResolveAsync(FieldResolutionContext context, CancellationToken // :( result = new InternalServerErrorGraphActionResult("Operation failed.", ex); } + finally + { + if (isolationObtained) + isolationManager.Release(); + } // resolve the final graph action output using the provided field context // in what ever manner is appropriate for the result itself diff --git a/src/graphql-aspnet/Internal/Resolvers/GraphDirectiveActionResolver.cs b/src/graphql-aspnet/Internal/Resolvers/GraphDirectiveActionResolver.cs index 6543bd4d7..db5213a48 100644 --- a/src/graphql-aspnet/Internal/Resolvers/GraphDirectiveActionResolver.cs +++ b/src/graphql-aspnet/Internal/Resolvers/GraphDirectiveActionResolver.cs @@ -18,9 +18,11 @@ namespace GraphQL.AspNet.Internal.Resolvers using GraphQL.AspNet.Controllers.ActionResults; using GraphQL.AspNet.Directives; using GraphQL.AspNet.Execution.Contexts; + using GraphQL.AspNet.Execution.Exceptions; using GraphQL.AspNet.Interfaces.Controllers; using GraphQL.AspNet.Interfaces.Execution; using GraphQL.AspNet.Interfaces.Internal; + using Microsoft.Extensions.DependencyInjection; /// /// A special resolver specifically for invoking controller actions @@ -51,6 +53,9 @@ public async Task ResolveAsync(DirectiveResolutionContext context, CancellationT return; IGraphActionResult result; + var isolationObtained = false; + IGraphQLFieldResolverIsolationManager isolationManager = null; + try { // create a directive instance for this invocation @@ -58,6 +63,10 @@ public async Task ResolveAsync(DirectiveResolutionContext context, CancellationT .ServiceProvider? .GetService(_directiveTemplate.ObjectType) as GraphDirective; + isolationManager = context + .ServiceProvider? + .GetService(); + if (directive == null) { // fallback: attempt to create the directive if it has no constructor parameters @@ -79,8 +88,23 @@ public async Task ResolveAsync(DirectiveResolutionContext context, CancellationT $"must also be registered to the service provider; Try using '{nameof(SchemaOptions.AddGraphType)}' " + $"with the type of your directive at startup."); } + else if (isolationManager == null) + { + throw new GraphExecutionException( + $"No {nameof(IGraphQLFieldResolverIsolationManager)} was configured for the request. " + + $"Unable to determine the isolation requirements for the directive '{_directiveTemplate.InternalFullName}'."); + } else { + var shouldIsolate = isolationManager + .ShouldIsolate(context.Schema, TypeTemplates.GraphFieldSource.Action); + + if (shouldIsolate) + { + await isolationManager.WaitAsync(); + isolationObtained = true; + } + // invoke the right action method and set a result. var task = directive.InvokeActionAsync(action, context); @@ -93,6 +117,11 @@ public async Task ResolveAsync(DirectiveResolutionContext context, CancellationT // :( result = new InternalServerErrorGraphActionResult("Operation failed.", ex); } + finally + { + if (isolationObtained) + isolationManager.Release(); + } // resolve the final graph action output using the provided field context // in what ever manner is appropriate for the result itself diff --git a/src/graphql-aspnet/Internal/Resolvers/InputValueResolverMethodGenerator.cs b/src/graphql-aspnet/Internal/Resolvers/InputValueResolverMethodGenerator.cs index a84bc1a3b..2bb8e8399 100644 --- a/src/graphql-aspnet/Internal/Resolvers/InputValueResolverMethodGenerator.cs +++ b/src/graphql-aspnet/Internal/Resolvers/InputValueResolverMethodGenerator.cs @@ -46,7 +46,7 @@ public InputValueResolverMethodGenerator(ISchema schema) public IInputValueResolver CreateResolver(GraphTypeExpression typeExpression) { // used for variable definitions - return this.CreateResolver(typeExpression, null); + return this.CreateResolverInternal(typeExpression, null); } /// @@ -57,7 +57,7 @@ public IInputValueResolver CreateResolver(GraphTypeExpression typeExpression) /// IQueryInputValueResolver. public IInputValueResolver CreateResolver(IInputGraphField field) { - return this.CreateResolver(field.TypeExpression, field); + return this.CreateResolverInternal(field.TypeExpression, field); } /// @@ -68,10 +68,10 @@ public IInputValueResolver CreateResolver(IInputGraphField field) /// IQueryInputValueResolver. public IInputValueResolver CreateResolver(IGraphArgument argument) { - return this.CreateResolver(argument.TypeExpression, argument); + return this.CreateResolverInternal(argument.TypeExpression, argument); } - private IInputValueResolver CreateResolver(GraphTypeExpression typeExpression, IDefaultValueSchemaItem defaultValueProvider) + private IInputValueResolver CreateResolverInternal(GraphTypeExpression typeExpression, IDefaultValueSchemaItem defaultValueProvider) { Validation.ThrowIfNull(typeExpression, nameof(typeExpression)); @@ -79,11 +79,17 @@ private IInputValueResolver CreateResolver(GraphTypeExpression typeExpression, I if (graphType == null) return null; - return this.CreateResolver(graphType, typeExpression, defaultValueProvider); + return this.CreateResolverInternal(graphType, typeExpression, defaultValueProvider); } - private IInputValueResolver CreateResolver(IGraphType graphType, GraphTypeExpression expression, IDefaultValueSchemaItem defaultValueProvider) + private IInputValueResolver CreateResolverInternal(IGraphType graphType, GraphTypeExpression expression, IDefaultValueSchemaItem defaultValueProvider, Dictionary trackedComplexResolvers = null) { + // keep a list of complex value resolvers that were generated in this run + // to prevent infinite loops for self referencing objects + // all instnaces where a complex object needs to be resolved will use + // the same referenced resolver, scoped to this single argument that is being generated + trackedComplexResolvers = trackedComplexResolvers ?? new Dictionary(); + // extract the core resolver for the input type being processed IInputValueResolver coreResolver = null; Type coreType = null; @@ -101,7 +107,7 @@ private IInputValueResolver CreateResolver(IGraphType graphType, GraphTypeExpres else if (graphType is IInputObjectGraphType inputType) { coreType = _schema.KnownTypes.FindConcreteType(inputType); - coreResolver = this.CreateInputObjectResolver(inputType, coreType, defaultValueProvider); + coreResolver = this.CreateInputObjectResolver(inputType, coreType, defaultValueProvider, trackedComplexResolvers); } // wrap any list wrappers around core resolver according to the type expression @@ -117,23 +123,22 @@ private IInputValueResolver CreateResolver(IGraphType graphType, GraphTypeExpres return coreResolver; } - private IInputValueResolver CreateInputObjectResolver(IInputObjectGraphType inputType, Type type, IDefaultValueSchemaItem defaultValueProvider) + private IInputValueResolver CreateInputObjectResolver( + IInputObjectGraphType inputType, + Type type, + IDefaultValueSchemaItem defaultValueProvider, + Dictionary trackedComplexResolvers) { + if (trackedComplexResolvers.TryGetValue(inputType, out var alreadyBuiltResolver)) + return alreadyBuiltResolver; + var inputObjectResolver = new InputObjectValueResolver(inputType, type, _schema, defaultValueProvider); + trackedComplexResolvers.Add(inputType, inputObjectResolver); foreach (var field in inputType.Fields) { - IInputValueResolver childResolver; - if (field.TypeExpression.TypeName == inputType.Name) - { - childResolver = inputObjectResolver; - } - else - { - var graphType = _schema.KnownTypes.FindGraphType(field.TypeExpression.TypeName); - childResolver = this.CreateResolver(graphType, field.TypeExpression, field); - } - + var graphType = _schema.KnownTypes.FindGraphType(field.TypeExpression.TypeName); + var childResolver = this.CreateResolverInternal(graphType, field.TypeExpression, field, trackedComplexResolvers); inputObjectResolver.AddFieldResolver(field.Name, childResolver); } diff --git a/src/graphql-aspnet/Internal/Resolvers/ObjectMethodGraphFieldResolver.cs b/src/graphql-aspnet/Internal/Resolvers/ObjectMethodGraphFieldResolver.cs index 14cf1bd43..2ce9f09fc 100644 --- a/src/graphql-aspnet/Internal/Resolvers/ObjectMethodGraphFieldResolver.cs +++ b/src/graphql-aspnet/Internal/Resolvers/ObjectMethodGraphFieldResolver.cs @@ -19,6 +19,7 @@ namespace GraphQL.AspNet.Internal.Resolvers using GraphQL.AspNet.Execution.Contexts; using GraphQL.AspNet.Execution.Exceptions; using GraphQL.AspNet.Interfaces.Execution; + using Microsoft.Extensions.DependencyInjection; /// /// A field resolver that will invoke a schema pipeline for whatever schema is beng processed @@ -70,8 +71,29 @@ public virtual async Task ResolveAsync(FieldResolutionContext context, Cancellat return; } + var isolationObtained = false; + IGraphQLFieldResolverIsolationManager isolationManager = null; + try { + isolationManager = context + .ServiceProvider? + .GetService(); + + if (isolationManager == null) + { + throw new GraphExecutionException( + $"No {nameof(IGraphQLFieldResolverIsolationManager)} was configured for the request. " + + $"Unable to determine the isolation requirements for the resolver of field '{context.Request.InvocationContext.Field.Route.Path}'"); + } + + var shouldIsolate = isolationManager.ShouldIsolate(context.Schema, context.Request.Field.FieldSource); + if (shouldIsolate) + { + await isolationManager.WaitAsync(); + isolationObtained = true; + } + object data = null; var paramSet = context.Arguments.PrepareArguments(_graphMethod); @@ -110,6 +132,11 @@ public virtual async Task ResolveAsync(FieldResolutionContext context, Cancellat context.Request.Origin, ex); } + finally + { + if (isolationObtained) + isolationManager.Release(); + } } /// diff --git a/src/graphql-aspnet/Internal/Resolvers/ObjectPropertyGraphFieldResolver.cs b/src/graphql-aspnet/Internal/Resolvers/ObjectPropertyGraphFieldResolver.cs index 202c982ec..1d4f6a630 100644 --- a/src/graphql-aspnet/Internal/Resolvers/ObjectPropertyGraphFieldResolver.cs +++ b/src/graphql-aspnet/Internal/Resolvers/ObjectPropertyGraphFieldResolver.cs @@ -19,6 +19,7 @@ namespace GraphQL.AspNet.Internal.Resolvers using GraphQL.AspNet.Execution.Contexts; using GraphQL.AspNet.Execution.Exceptions; using GraphQL.AspNet.Interfaces.Execution; + using Microsoft.Extensions.DependencyInjection; /// /// A resolver that extracts a property from an object and returns it as a field value. @@ -88,8 +89,29 @@ public async Task ResolveAsync(FieldResolutionContext context, CancellationToken return; } + var isolationObtained = false; + IGraphQLFieldResolverIsolationManager isolationManager = null; + try { + isolationManager = context + .ServiceProvider? + .GetService(); + + if (isolationManager == null) + { + throw new GraphExecutionException( + $"No {nameof(IGraphQLFieldResolverIsolationManager)} was configured for the request. " + + $"Unable to determine the isolation requirements for the resolver of field '{context.Request.InvocationContext.Field.Route.Path}'"); + } + + var shouldIsolate = isolationManager.ShouldIsolate(context.Schema, context.Request.Field.FieldSource); + if (shouldIsolate) + { + await isolationManager.WaitAsync(); + isolationObtained = true; + } + var invoker = InstanceFactory.CreateInstanceMethodInvoker(_graphMethod.Method); var invokeReturn = invoker(ref sourceData, new object[0]); if (_graphMethod.IsAsyncField) @@ -133,6 +155,11 @@ public async Task ResolveAsync(FieldResolutionContext context, CancellationToken context.Request.Origin, ex); } + finally + { + if (isolationObtained) + isolationManager.Release(); + } } /// diff --git a/src/graphql-aspnet/Internal/TypeTemplates/InputObjectGraphTypeTemplate.cs b/src/graphql-aspnet/Internal/TypeTemplates/InputObjectGraphTypeTemplate.cs index 10c13c246..58703e44b 100644 --- a/src/graphql-aspnet/Internal/TypeTemplates/InputObjectGraphTypeTemplate.cs +++ b/src/graphql-aspnet/Internal/TypeTemplates/InputObjectGraphTypeTemplate.cs @@ -155,7 +155,7 @@ private bool CanBeInputField(PropertyInfo propInfo) if (propInfo == null) return false; - if (propInfo.Attributes.SingleAttributeOrDefault() != null) + if (propInfo.HasAttribute()) return false; if (Constants.IgnoredFieldNames.Contains(propInfo.Name)) diff --git a/src/graphql-aspnet/Internal/TypeTemplates/NonLeafGraphTypeTemplateBase.cs b/src/graphql-aspnet/Internal/TypeTemplates/NonLeafGraphTypeTemplateBase.cs index 3f7458f41..a83aa387b 100644 --- a/src/graphql-aspnet/Internal/TypeTemplates/NonLeafGraphTypeTemplateBase.cs +++ b/src/graphql-aspnet/Internal/TypeTemplates/NonLeafGraphTypeTemplateBase.cs @@ -188,6 +188,12 @@ protected virtual bool CouldBeGraphField(MemberInfo memberInfo) if (Constants.IgnoredFieldNames.Contains(memberInfo.Name)) return false; + if (memberInfo.DeclaringType?.IsRecord() ?? false) + { + if (Constants.IgnoredRecordFieldNames.Contains(memberInfo.Name)) + return false; + } + // when the member declares any known attribute in the library include it // and allow it to generate validation failures if its not properly constructed if (memberInfo.SingleAttributeOfTypeOrDefault() != null) diff --git a/src/graphql-aspnet/Middleware/FieldExecution/Components/ProcessChildFieldsMiddleware.cs b/src/graphql-aspnet/Middleware/FieldExecution/Components/ProcessChildFieldsMiddleware.cs index 9a508376d..ac2127eac 100644 --- a/src/graphql-aspnet/Middleware/FieldExecution/Components/ProcessChildFieldsMiddleware.cs +++ b/src/graphql-aspnet/Middleware/FieldExecution/Components/ProcessChildFieldsMiddleware.cs @@ -118,7 +118,7 @@ private async Task ProcessDownStreamFieldContextsAsync(GraphFieldExecutionContex var sourceItemLookup = this.MapExpectedConcreteTypeFromSourceItem(allSourceItems, graphType); // all the child field contexts that need to execute - var executableChildContexts = new SortedFieldExecutionContextList(); + var executableChildContexts = new List(); for (var i = 0; i < context.InvocationContext.ChildContexts.Count; i++) { @@ -170,44 +170,16 @@ private async Task ProcessDownStreamFieldContextsAsync(GraphFieldExecutionContex // Step 3 // -------------------- // Stage the contexts to be executed - foreach (var childToExecute in orderedContexts) - { - executableChildContexts.Add( - childToExecute, - _resolversToIsolate.ShouldIsolateFieldSource(childToExecute.Field.FieldSource)); - } + executableChildContexts.AddRange(orderedContexts); } // Step 4 // --------------------------------- - // Execute all the child contexts. Isolating those that need to be - // and executing them first through to completion - foreach (var isolatedChildContext in executableChildContexts.IsolatedContexts) + // Execute all the child contexts. Order doesn't matter + foreach (var isolatedChildContext in executableChildContexts) { var task = this.ExecuteChildContextAsync(context, isolatedChildContext, cancelToken); allChildPipelines.Add(task); - - if (_debugMode) - { - await task.ConfigureAwait(false); - } - else - { - // await the isolated task to prevent any potential paralellization - // by the task system but not in such a way that a faulted task would - // throw an execution. Allow the results (exceptions included) to be - // captured by CaptureChildFieldExecutionResults - await Task.WhenAll(task); - } - } - - foreach (var paralellChildContext in executableChildContexts.ParalellContexts) - { - var task = this.ExecuteChildContextAsync(context, paralellChildContext, cancelToken); - allChildPipelines.Add(task); - - if (_debugMode) - await task.ConfigureAwait(false); } await Task.WhenAll(allChildPipelines).ConfigureAwait(false); diff --git a/src/graphql-aspnet/Middleware/QueryExecution/Components/ExecuteQueryOperationMiddleware.cs b/src/graphql-aspnet/Middleware/QueryExecution/Components/ExecuteQueryOperationMiddleware.cs index 0782061cf..aff0d184f 100644 --- a/src/graphql-aspnet/Middleware/QueryExecution/Components/ExecuteQueryOperationMiddleware.cs +++ b/src/graphql-aspnet/Middleware/QueryExecution/Components/ExecuteQueryOperationMiddleware.cs @@ -26,6 +26,7 @@ namespace GraphQL.AspNet.Middleware.QueryExecution.Components using GraphQL.AspNet.Interfaces.Schema; using GraphQL.AspNet.Schemas.Structural; using GraphQL.AspNet.Schemas.TypeSystem; + using GraphQL.AspNet.Execution.RulesEngine.RuleSets.DocumentValidation.QueryFragmentSteps; /// /// Begins executing the top level fields, of the operation on the context, through the field execution pipeline. @@ -45,7 +46,6 @@ private class FieldPipelineInvocation private readonly ISchemaPipeline _fieldExecutionPipeline; private readonly TSchema _schema; - private readonly ResolverIsolationOptions _isolationOptions; private readonly bool _debugMode; private readonly TimeSpan? _queryTimeout; @@ -57,7 +57,6 @@ private class FieldPipelineInvocation public ExecuteQueryOperationMiddleware(TSchema schema, ISchemaPipeline fieldExecutionPipeline) { _schema = Validation.ThrowIfNullOrReturn(schema, nameof(schema)); - _isolationOptions = _schema.Configuration.ExecutionOptions.ResolverIsolation; _debugMode = _schema.Configuration.ExecutionOptions.DebugMode; _queryTimeout = _schema.Configuration.ExecutionOptions.QueryTimeout; _fieldExecutionPipeline = Validation.ThrowIfNullOrReturn(fieldExecutionPipeline, nameof(fieldExecutionPipeline)); @@ -97,34 +96,20 @@ private async Task ExecuteOperationAsync(QueryExecutionContext context) // Step 0 // -------------------------- - // Sort the top level requested fields of this operation such that - // those contexts that should be isolated execute first and all others - // run in paralell - IEnumerable<(IGraphFieldInvocationContext Context, bool ExecuteIsolated)> orderedContextList; - if (operation.OperationType == GraphOperationType.Mutation) - { - // top level mutation operatons must be executed in sequential order - // due to potential side effects on the underlying data - // https://graphql.github.io/graphql-spec/October2021/#sec-Mutation - orderedContextList = operation.FieldContexts - .Select(x => (x, true)); - } - else - { - // with non-mutation queries, order the contexts such that the isolated ones (as determined by - // the configuration for this schema) are on top. All contexts are ran in isolation. - // However, when in debug mode all top level queries are run in isolation. - orderedContextList = operation - .FieldContexts - .Select(x => (x, _debugMode || _isolationOptions.ShouldIsolateFieldSource(x.Field.FieldSource))) - .OrderByDescending(x => x.Item2); - } + + // top level mutation operatons must be executed in sequential order + // due to potential side effects on the underlying data + // https://graphql.github.io/graphql-spec/October2021/#sec-Mutation + // + // or if we're in debug mode isolate all type level contexts + bool contextsMustBeIsolated = _debugMode || operation.OperationType == GraphOperationType.Mutation; + var contextList = operation.FieldContexts.ToList(); // Step 1 // -------------------------- // Begin a field execution pipeline for each top level field // those fields will then call their child fields in turn - foreach (var item in orderedContextList) + foreach (var item in contextList) { // if at any point the context token signals a cancellation // the monitor will switch out of a running state, @@ -133,25 +118,25 @@ private async Task ExecuteOperationAsync(QueryExecutionContext context) break; var path = new SourcePath(); - path.AddFieldName(item.Context.Name); + path.AddFieldName(item.Name); object dataSourceValue; // fetch the source data value to use for the field invocation // attempt to retrieve from the master context if it was supplied by the pipeline // invoker, otherwise generate a root source - if (!context.DefaultFieldSources.TryRetrieveSource(item.Context.Field, out dataSourceValue)) + if (!context.DefaultFieldSources.TryRetrieveSource(item.Field, out dataSourceValue)) dataSourceValue = this.GenerateRootSourceData(operation.OperationType); - var topLevelDataItem = new FieldDataItem(item.Context, dataSourceValue, path); + var topLevelDataItem = new FieldDataItem(item, dataSourceValue, path); var sourceData = new FieldDataItemContainer(dataSourceValue, path, topLevelDataItem); var fieldRequest = new GraphFieldRequest( context.QueryRequest, - item.Context, + item, sourceData, - new SourceOrigin(item.Context.Origin.Location, path)); + new SourceOrigin(item.Origin.Location, path)); var fieldContext = new GraphFieldExecutionContext( context, @@ -174,11 +159,7 @@ private async Task ExecuteOperationAsync(QueryExecutionContext context) fieldInvocations.Add(pipelineInvocation); fieldInvocationTasks.Add(pipelineInvocation.Task); - if (_debugMode) - { - await fieldTask.ConfigureAwait(false); - } - else if (item.ExecuteIsolated) + if (contextsMustBeIsolated) { // await the isolated task to prevent any potential paralellization // by the task system but not in such a way that a faulted task would diff --git a/src/graphql-aspnet/Schemas/GraphTypeExpression_Statics.cs b/src/graphql-aspnet/Schemas/GraphTypeExpression_Statics.cs index 2f89136b1..238876dff 100644 --- a/src/graphql-aspnet/Schemas/GraphTypeExpression_Statics.cs +++ b/src/graphql-aspnet/Schemas/GraphTypeExpression_Statics.cs @@ -156,19 +156,39 @@ public static GraphTypeExpression FromDeclaration(ReadOnlySpan typeExpress /// An optional set of wrappers to use as a set of overrides on the type provided. /// GraphFieldOptions. public static GraphTypeExpression FromType(Type typeToCheck, MetaGraphTypes[] typeWrappers = null) + { + return FromType(typeToCheck, TypeKind.OBJECT, typeWrappers); + } + + /// + /// Inspects the provided type and generates a type expression to represent it in the object grpah. + /// + /// The complete type specification to check. + /// An explicit typekind to use when determining the name of the + /// that will be included in the expression. + /// An optional set of wrappers to use as a set of overrides on the type provided. + /// GraphFieldOptions. + public static GraphTypeExpression FromType(Type typeToCheck, TypeKind typeKind, MetaGraphTypes[] typeWrappers = null) + { + var name = GraphTypeNames.ParseName(typeToCheck, typeKind); + return FromType(typeToCheck, name, typeWrappers); + } + + /// + /// Inspects the provided type and generates a type expression to represent it in the object grpah. + /// + /// The complete type specification to check. + /// An explicit typename to use in the type expression. This value + /// will override any name gleaned from . + /// An optional set of wrappers to use as a set of overrides on the type provided. + /// GraphFieldOptions. + public static GraphTypeExpression FromType(Type typeToCheck, string typeName, MetaGraphTypes[] typeWrappers = null) { Validation.ThrowIfNull(typeToCheck, nameof(typeToCheck)); + typeName = Validation.ThrowIfNullWhiteSpaceOrReturn(typeName, nameof(typeName)); if (typeWrappers != null) - { - typeToCheck = GraphValidation.EliminateWrappersFromCoreType( - typeToCheck, - eliminateEnumerables: true, - eliminateTask: true, - eliminateNullableT: true); - - return new GraphTypeExpression(typeToCheck.FriendlyGraphTypeName(), typeWrappers); - } + return new GraphTypeExpression(typeName, typeWrappers); // strip out Task{T} before doin any type inspections typeToCheck = GraphValidation.EliminateWrappersFromCoreType( @@ -207,13 +227,7 @@ public static GraphTypeExpression FromType(Type typeToCheck, MetaGraphTypes[] ty wrappers.Add(MetaGraphTypes.IsNotNull); } - typeToCheck = GraphValidation.EliminateWrappersFromCoreType( - typeToCheck, - eliminateEnumerables: false, - eliminateTask: false, - eliminateNullableT: true); - - return new GraphTypeExpression(typeToCheck.FriendlyGraphTypeName(), wrappers); + return new GraphTypeExpression(typeName, wrappers); } /// diff --git a/src/graphql-aspnet/Schemas/Structural/GraphFieldArgumentCollection.cs b/src/graphql-aspnet/Schemas/Structural/GraphFieldArgumentCollection.cs index 519cb6692..38b33ac68 100644 --- a/src/graphql-aspnet/Schemas/Structural/GraphFieldArgumentCollection.cs +++ b/src/graphql-aspnet/Schemas/Structural/GraphFieldArgumentCollection.cs @@ -14,10 +14,11 @@ namespace GraphQL.AspNet.Schemas.Structural using System.Collections.Generic; using System.Diagnostics; using GraphQL.AspNet.Common; - using GraphQL.AspNet.Common.Generics; using GraphQL.AspNet.Interfaces.Schema; using GraphQL.AspNet.Schemas.TypeSystem; + using OrderedDictionaryOfStringAndGraphArgument = GraphQL.AspNet.Common.Generics.OrderedDictionary; + /// /// A collection of allowed arguments defined for a or . /// @@ -25,7 +26,7 @@ namespace GraphQL.AspNet.Schemas.Structural internal class GraphFieldArgumentCollection : IGraphArgumentCollection { private readonly ISchemaItem _owner; - private readonly OrderedDictionary _arguments; + private readonly OrderedDictionaryOfStringAndGraphArgument _arguments; private IGraphArgument _sourceArgument; /// @@ -36,7 +37,7 @@ internal class GraphFieldArgumentCollection : IGraphArgumentCollection public GraphFieldArgumentCollection(ISchemaItem owner) { _owner = Validation.ThrowIfNullOrReturn(owner, nameof(owner)); - _arguments = new OrderedDictionary(StringComparer.Ordinal); + _arguments = new OrderedDictionaryOfStringAndGraphArgument(StringComparer.Ordinal); } /// diff --git a/src/graphql-aspnet/Schemas/Structural/SchemaItemPath.cs b/src/graphql-aspnet/Schemas/Structural/SchemaItemPath.cs index c3ce06bc9..dafe15709 100644 --- a/src/graphql-aspnet/Schemas/Structural/SchemaItemPath.cs +++ b/src/graphql-aspnet/Schemas/Structural/SchemaItemPath.cs @@ -87,7 +87,6 @@ private void EnsurePathInitialized() if (_pathInitialized) return; - _pathInitialized = true; var workingPath = SchemaItemPath.NormalizeFragment(this.Raw); // split the path into its fragments @@ -137,7 +136,7 @@ private void EnsurePathInitialized() } // ensure each fragment matches the naming specification - foreach (var fragment in pathFragments.Skip(this.RootCollection == SchemaItemCollections.Unknown ? 0 : 1)) + foreach (var fragment in pathFragments.Skip(_rootCollection == SchemaItemCollections.Unknown ? 0 : 1)) { if (!this.ValidateFragment(fragment)) return; @@ -147,9 +146,10 @@ private void EnsurePathInitialized() if (pathFragments.Count > 1) _parentField = new SchemaItemPath(string.Join(RouteConstants.PATH_SEPERATOR, pathFragments.Take(pathFragments.Count - 1))); - _isTopLevelField = pathFragments.Count == 1 || (pathFragments.Count == 2 && this.RootCollection > SchemaItemCollections.Unknown); // e.g. "[query]/name" - _isValid = this.Name.Length > 0; + _isTopLevelField = pathFragments.Count == 1 || (pathFragments.Count == 2 && _rootCollection > SchemaItemCollections.Unknown); // e.g. "[query]/name" + _isValid = _name.Length > 0; _path = this.GeneratePathString(pathFragments); + _pathInitialized = true; } } diff --git a/src/graphql-aspnet/Schemas/TypeSystem/Introspection/Model/IntrospectedSchema.cs b/src/graphql-aspnet/Schemas/TypeSystem/Introspection/Model/IntrospectedSchema.cs index 0eff5c0bb..821a66f69 100644 --- a/src/graphql-aspnet/Schemas/TypeSystem/Introspection/Model/IntrospectedSchema.cs +++ b/src/graphql-aspnet/Schemas/TypeSystem/Introspection/Model/IntrospectedSchema.cs @@ -16,11 +16,12 @@ namespace GraphQL.AspNet.Schemas.TypeSystem.Introspection.Model using GraphQL.AspNet.Attributes; using GraphQL.AspNet.Common; using GraphQL.AspNet.Common.Extensions; - using GraphQL.AspNet.Common.Generics; using GraphQL.AspNet.Execution.Exceptions; using GraphQL.AspNet.Interfaces.Schema; using GraphQL.AspNet.Schemas.TypeSystem; + using OrderedDictionaryOfStringAndType = GraphQL.AspNet.Common.Generics.OrderedDictionary; + /// /// A model object containing data for the '__Schema' type. /// @@ -28,7 +29,7 @@ namespace GraphQL.AspNet.Schemas.TypeSystem.Introspection.Model public sealed class IntrospectedSchema : IntrospectedItem, ISchemaItem { private readonly ISchema _schema; - private OrderedDictionary _typeList; + private OrderedDictionaryOfStringAndType _typeList; private List _directiveList; /// @@ -39,7 +40,7 @@ public IntrospectedSchema(ISchema schema) : base(schema) { _schema = Validation.ThrowIfNullOrReturn(schema, nameof(schema)); - _typeList = new OrderedDictionary(); + _typeList = new OrderedDictionaryOfStringAndType(); } /// @@ -72,7 +73,7 @@ private void RebuildDirectiveList() private void RebuildIntrospectedTypeList() { _typeList.Clear(); - _typeList = new OrderedDictionary(); + _typeList = new OrderedDictionaryOfStringAndType(); // build in a two pass approach // ----------------------------------------- diff --git a/src/graphql-aspnet/ServerExtensions/MultipartRequests/Engine/MultipartRequestGraphQLHttpProcessor.cs b/src/graphql-aspnet/ServerExtensions/MultipartRequests/Engine/MultipartRequestGraphQLHttpProcessor.cs index 49978b481..2aa481df2 100644 --- a/src/graphql-aspnet/ServerExtensions/MultipartRequests/Engine/MultipartRequestGraphQLHttpProcessor.cs +++ b/src/graphql-aspnet/ServerExtensions/MultipartRequests/Engine/MultipartRequestGraphQLHttpProcessor.cs @@ -285,7 +285,7 @@ protected virtual async Task WriteQueryResponseAsync( this.Response.ContentType = Constants.MediaTypes.JSON; if (this.Schema.Configuration.ResponseOptions.AppendServerHeader) { - this.Response.Headers.Add(Constants.ServerInformation.SERVER_INFORMATION_HEADER, Constants.ServerInformation.ServerData); + this.Response.Headers.Append(Constants.ServerInformation.SERVER_INFORMATION_HEADER, Constants.ServerInformation.ServerData); } var localWriter = renderAsBatch diff --git a/src/graphql-aspnet/Web/GraphQLHttpProcessorBase.cs b/src/graphql-aspnet/Web/GraphQLHttpProcessorBase.cs index 43cbef69f..2fe5e660a 100644 --- a/src/graphql-aspnet/Web/GraphQLHttpProcessorBase.cs +++ b/src/graphql-aspnet/Web/GraphQLHttpProcessorBase.cs @@ -111,7 +111,7 @@ protected async Task WriteStatusCodeResponseAsync(HttpStatusCode statusCode, str if (this.Schema.Configuration.ResponseOptions.AppendServerHeader) { - this.Response.Headers.Add(Constants.ServerInformation.SERVER_INFORMATION_HEADER, Constants.ServerInformation.ServerData); + this.Response.Headers.Append(Constants.ServerInformation.SERVER_INFORMATION_HEADER, Constants.ServerInformation.ServerData); } this.Response.StatusCode = (int)statusCode; diff --git a/src/library-common.props b/src/library-common.props index 70e6b7b20..31a4b789c 100644 --- a/src/library-common.props +++ b/src/library-common.props @@ -1,7 +1,7 @@ - net7.0;net6.0;netstandard2.0; + net9.0;net8.0;netstandard2.0; latest $(NoWarn);1701;1702;1705;1591;NU1603;IDE0019;IDE0017;RCS1146;RCS1194; @@ -55,10 +55,10 @@ - + - + @@ -68,7 +68,7 @@ - + diff --git a/src/library-tests.props b/src/library-tests.props new file mode 100644 index 000000000..0b086b1ea --- /dev/null +++ b/src/library-tests.props @@ -0,0 +1,27 @@ + + + + net9.0;net8.0; + latest + $(NoWarn);1701;1702;1705;1591;NU1603;IDE0019;IDE0017;RCS1146;RCS1194; + GraphQL.AspNet.Tests + true + true + false + + + + + + + + $(MSBuildThisFileDirectory)\styles.ruleset + + + + + + + + + \ No newline at end of file diff --git a/src/unit-tests/graphql-aspnet-subscriptions-tests/GlobalSuppressions.cs b/src/unit-tests/graphql-aspnet-subscriptions-tests/GlobalSuppressions.cs index fe622bfbe..96f0b20e6 100644 --- a/src/unit-tests/graphql-aspnet-subscriptions-tests/GlobalSuppressions.cs +++ b/src/unit-tests/graphql-aspnet-subscriptions-tests/GlobalSuppressions.cs @@ -15,6 +15,12 @@ Justification = "Documenting test methods is unwarranted at this time.", Scope = "module")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage( + "StyleCop.CSharp.DocumentationRules", + "SA1601:Elements should be documented", + Justification = "Documenting test methods is unwarranted at this time.", + Scope = "module")] + [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage( "StyleCop.CSharp.NamingRules", "SA1313:Parameter names should begin with lower-case letter", diff --git a/src/unit-tests/graphql-aspnet-subscriptions-tests/SubscriptionReceiverExtensionTests.cs b/src/unit-tests/graphql-aspnet-subscriptions-tests/SubscriptionReceiverExtensionTests.cs index c2b9432c9..e47b57758 100644 --- a/src/unit-tests/graphql-aspnet-subscriptions-tests/SubscriptionReceiverExtensionTests.cs +++ b/src/unit-tests/graphql-aspnet-subscriptions-tests/SubscriptionReceiverExtensionTests.cs @@ -40,9 +40,9 @@ namespace GraphQL.AspNet.Tests public class SubscriptionReceiverExtensionTests { private ( - ISchemaBuilder, - ISchemaPipelineBuilder, QueryExecutionContext>, - ISchemaPipelineBuilder, GraphFieldExecutionContext>) + ISchemaBuilder SchemaBuilder, + ISchemaPipelineBuilder, QueryExecutionContext> QueryPipelineBuilder, + ISchemaPipelineBuilder, GraphFieldExecutionContext> FieldPipelineBuilder) CreateSchemaBuilderMock(SchemaOptions options) { var queryPipeline = Substitute.For, QueryExecutionContext>>(); diff --git a/src/unit-tests/graphql-aspnet-subscriptions-tests/SubscriptionServer/Protocols/GraphqlTransportWs/GqltwsClientAsserts.cs b/src/unit-tests/graphql-aspnet-subscriptions-tests/SubscriptionServer/Protocols/GraphqlTransportWs/GqltwsClientAsserts.cs index 9cd2488b7..a902cb612 100644 --- a/src/unit-tests/graphql-aspnet-subscriptions-tests/SubscriptionServer/Protocols/GraphqlTransportWs/GqltwsClientAsserts.cs +++ b/src/unit-tests/graphql-aspnet-subscriptions-tests/SubscriptionServer/Protocols/GraphqlTransportWs/GqltwsClientAsserts.cs @@ -25,12 +25,13 @@ public static class GqltwsClientAsserts /// The connection. /// The type of message to check for. /// if true, the message is removed from the queue. - internal static void AssertGqltwsResponse( + /// The raw string data (usually json formatted) of the message data recevied by the connection proxy + internal static string AssertGqltwsResponse( this MockClientConnection connection, GqltwsMessageType type, bool dequeue = true) { - connection.AssertGqltwsResponse(type, null, false, null, false, dequeue); + return connection.AssertGqltwsResponse(type, null, false, null, false, dequeue); } /// @@ -40,13 +41,14 @@ internal static void AssertGqltwsResponse( /// The type of message to check for. /// The id returned by the server, if supplied. /// if true, the message is removed from the queue. - internal static void AssertGqltwsResponse( + /// The raw string data (usually json formatted) of the message data recevied by the connection proxy + internal static string AssertGqltwsResponse( this MockClientConnection connection, GqltwsMessageType type, string id, bool dequeue = true) { - connection.AssertGqltwsResponse(type, id, true, null, false, dequeue); + return connection.AssertGqltwsResponse(type, id, true, null, false, dequeue); } /// @@ -58,17 +60,18 @@ internal static void AssertGqltwsResponse( /// The expected identifier of the subscription that rendered the data. /// The expected payload of the message, converted to a json string. /// if set to true if the message should be removed from the queue. - internal static void AssertGqltwsResponse( + /// The raw string data (usually json formatted) of the message data recevied by the connection proxy + internal static string AssertGqltwsResponse( this MockClientConnection connection, GqltwsMessageType type, string id, string expectedPayloadJson, bool dequeue = true) { - connection.AssertGqltwsResponse(type, id, true, expectedPayloadJson, true, dequeue); + return connection.AssertGqltwsResponse(type, id, true, expectedPayloadJson, true, dequeue); } - private static void AssertGqltwsResponse( + private static string AssertGqltwsResponse( this MockClientConnection connection, GqltwsMessageType type, string id, @@ -81,14 +84,14 @@ private static void AssertGqltwsResponse( Assert.Fail("No messages queued."); var message = dequeue ? connection.DequeueNextReceivedMessage() : connection.PeekNextReceivedMessage(); - var str = Encoding.UTF8.GetString(message.Data); + var rawMessageData = Encoding.UTF8.GetString(message.Data); var options = new JsonSerializerOptions(); options.PropertyNameCaseInsensitive = true; options.AllowTrailingCommas = true; options.Converters.Add(new GqltwsResponseMessageConverter()); - var convertedMessage = JsonSerializer.Deserialize(str, options); + var convertedMessage = JsonSerializer.Deserialize(rawMessageData, options); Assert.IsNotNull(convertedMessage, "Could not deserialize response message"); Assert.AreEqual(type, convertedMessage.Type, $"Expected message type of {type.ToString()} but got {convertedMessage.Type.ToString()}"); @@ -103,6 +106,8 @@ private static void AssertGqltwsResponse( if (compareId) Assert.AreEqual(id, convertedMessage.Id); + + return rawMessageData; } } } \ No newline at end of file diff --git a/src/unit-tests/graphql-aspnet-subscriptions-tests/SubscriptionServer/Protocols/GraphqlTransportWs/GqltwsClientProxyTests.cs b/src/unit-tests/graphql-aspnet-subscriptions-tests/SubscriptionServer/Protocols/GraphqlTransportWs/GqltwsClientProxyTests.cs index 6b3bff228..533890d27 100644 --- a/src/unit-tests/graphql-aspnet-subscriptions-tests/SubscriptionServer/Protocols/GraphqlTransportWs/GqltwsClientProxyTests.cs +++ b/src/unit-tests/graphql-aspnet-subscriptions-tests/SubscriptionServer/Protocols/GraphqlTransportWs/GqltwsClientProxyTests.cs @@ -34,7 +34,7 @@ namespace GraphQL.AspNet.Tests.SubscriptionServer.Protocols.GraphqlTransportWs [TestFixture] public partial class GqltwsClientProxyTests { - private (MockClientConnection, GqltwsClientProxy, ISubscriptionEventRouter) CreateConnection() + private (MockClientConnection ClientConnection, GqltwsClientProxy ClientProxy, ISubscriptionEventRouter Router) CreateConnection() { var server = new TestServerBuilder() .AddGraphController() @@ -746,5 +746,23 @@ public async Task Deserialize_InvalidMessage_ProcessesUnknownMessage() connection.AssertServerClosedConnection((ConnectionCloseStatus)GqltwsConstants.CustomCloseEventIds.InvalidMessageType); graphqlWsClient.Dispose(); } + + [Test] + public async Task SendConnectionInitMessage_RespondsWithCorrectAckMessage() + { + using var restorePoint = new GraphQLGlobalSubscriptionRestorePoint(); + (var socketClient, var client, var router) = this.CreateConnection(); + + socketClient.QueueClientMessage(new GqltwsClientConnectionInitMessage()); + socketClient.QueueConnectionClosedByClient(); + + await client.StartConnectionAsync(); + + var rawMessageData = socketClient.AssertGqltwsResponse(GqltwsMessageType.CONNECTION_ACK); + socketClient.AssertClientClosedConnection(); + + var expectedData = "{ \"type\": \"connection_ack\" }"; + CommonAssertions.AreEqualJsonStrings(expectedData, rawMessageData); + } } } \ No newline at end of file diff --git a/src/unit-tests/graphql-aspnet-subscriptions-tests/SubscriptionServer/Protocols/GraphqlWsLegacy/GraphqlWsLegacyClientAsserts.cs b/src/unit-tests/graphql-aspnet-subscriptions-tests/SubscriptionServer/Protocols/GraphqlWsLegacy/GraphqlWsLegacyClientAsserts.cs index ca3609107..45937a07f 100644 --- a/src/unit-tests/graphql-aspnet-subscriptions-tests/SubscriptionServer/Protocols/GraphqlWsLegacy/GraphqlWsLegacyClientAsserts.cs +++ b/src/unit-tests/graphql-aspnet-subscriptions-tests/SubscriptionServer/Protocols/GraphqlWsLegacy/GraphqlWsLegacyClientAsserts.cs @@ -25,12 +25,13 @@ public static class GraphqlWsLegacyClientAsserts /// The connection. /// The type of message to check for. /// if true, the message is removed from the queue. - internal static void AssertGraphqlWsLegacyResponse( + /// The raw string data (usually json formatted) of the message data recevied by the connection proxy + internal static string AssertGraphqlWsLegacyResponse( this MockClientConnection connection, GraphqlWsLegacyMessageType type, bool dequeue = true) { - connection.AssertGraphqlWsLegacyResponse(type, null, false, null, false, dequeue); + return connection.AssertGraphqlWsLegacyResponse(type, null, false, null, false, dequeue); } /// @@ -40,13 +41,14 @@ internal static void AssertGraphqlWsLegacyResponse( /// The type of message to check for. /// The id returned by the server, if supplied. /// if true, the message is removed from the queue. - internal static void AssertGraphqlWsLegacyResponse( + /// The raw string data (usually json formatted) of the message data recevied by the connection proxy + internal static string AssertGraphqlWsLegacyResponse( this MockClientConnection connection, GraphqlWsLegacyMessageType type, string id, bool dequeue = true) { - connection.AssertGraphqlWsLegacyResponse(type, id, true, null, false, dequeue); + return connection.AssertGraphqlWsLegacyResponse(type, id, true, null, false, dequeue); } /// @@ -58,17 +60,18 @@ internal static void AssertGraphqlWsLegacyResponse( /// The expected identifier of the subscription that rendered the data. /// The expected payload of the message, converted to a json string. /// if set to true if the message should be removed from the queue. - internal static void AssertGraphqlWsLegacyResponse( + /// The raw string data (usually json formatted) of the message data recevied by the connection proxy + internal static string AssertGraphqlWsLegacyResponse( this MockClientConnection connection, GraphqlWsLegacyMessageType type, string id, string expectedPayloadJson, bool dequeue = true) { - connection.AssertGraphqlWsLegacyResponse(type, id, true, expectedPayloadJson, true, dequeue); + return connection.AssertGraphqlWsLegacyResponse(type, id, true, expectedPayloadJson, true, dequeue); } - private static void AssertGraphqlWsLegacyResponse( + private static string AssertGraphqlWsLegacyResponse( this MockClientConnection connection, GraphqlWsLegacyMessageType type, string id, @@ -81,14 +84,14 @@ private static void AssertGraphqlWsLegacyResponse( Assert.Fail("No messages queued."); var message = dequeue ? connection.DequeueNextReceivedMessage() : connection.PeekNextReceivedMessage(); - var str = Encoding.UTF8.GetString(message.Data); + var rawData = Encoding.UTF8.GetString(message.Data); var options = new JsonSerializerOptions(); options.PropertyNameCaseInsensitive = true; options.AllowTrailingCommas = true; options.Converters.Add(new GraphqlWsLegacyResponseMessageConverter()); - var convertedMessage = JsonSerializer.Deserialize(str, options); + var convertedMessage = JsonSerializer.Deserialize(rawData, options); Assert.IsNotNull(convertedMessage, "Could not deserialized response message"); Assert.AreEqual(type, convertedMessage.Type, $"Expected message type of {type.ToString()} but got {convertedMessage.Type.ToString()}"); @@ -103,6 +106,8 @@ private static void AssertGraphqlWsLegacyResponse( if (compareId) Assert.AreEqual(id, convertedMessage.Id); + + return rawData; } } } \ No newline at end of file diff --git a/src/unit-tests/graphql-aspnet-subscriptions-tests/SubscriptionServer/Protocols/GraphqlWsLegacy/GraphqlWsLegacyClientProxyTests.cs b/src/unit-tests/graphql-aspnet-subscriptions-tests/SubscriptionServer/Protocols/GraphqlWsLegacy/GraphqlWsLegacyClientProxyTests.cs index 202d501a3..0547ee6ea 100644 --- a/src/unit-tests/graphql-aspnet-subscriptions-tests/SubscriptionServer/Protocols/GraphqlWsLegacy/GraphqlWsLegacyClientProxyTests.cs +++ b/src/unit-tests/graphql-aspnet-subscriptions-tests/SubscriptionServer/Protocols/GraphqlWsLegacy/GraphqlWsLegacyClientProxyTests.cs @@ -31,7 +31,7 @@ namespace GraphQL.AspNet.Tests.SubscriptionServer.Protocols.GraphqlWsLegacy [TestFixture] public partial class GraphqlWsLegacyClientProxyTests { - private (MockClientConnection, GraphqlWsLegacyClientProxy, ISubscriptionEventRouter) CreateConnection() + private (MockClientConnection ClientConnection, GraphqlWsLegacyClientProxy ClientProxy, ISubscriptionEventRouter Router) CreateConnection() { var server = new TestServerBuilder() .AddController() @@ -501,5 +501,23 @@ public async Task Deserialize_InvalidMessage_ProcessesUnknownMessage() socketClient.AssertGraphqlWsLegacyResponse(GraphqlWsLegacyMessageType.ERROR); socketClient.AssertClientClosedConnection(); } + + [Test] + public async Task SendConnectionInitMessage_RespondsWithCorrectAckMessage() + { + using var restorePoint = new GraphQLGlobalSubscriptionRestorePoint(); + (var socketClient, var client, var router) = this.CreateConnection(); + + socketClient.QueueClientMessage(new GraphqlWsLegacyClientConnectionInitMessage()); + socketClient.QueueConnectionClosedByClient(); + + await client.StartConnectionAsync(); + + var rawMessageData = socketClient.AssertGraphqlWsLegacyResponse(GraphqlWsLegacyMessageType.CONNECTION_ACK); + socketClient.AssertClientClosedConnection(); + + var expectedData = "{ \"type\": \"connection_ack\" }"; + CommonAssertions.AreEqualJsonStrings(expectedData, rawMessageData); + } } } \ No newline at end of file diff --git a/src/unit-tests/graphql-aspnet-subscriptions-tests/Web/WebSockets/WebSocketClientConnectionTests.cs b/src/unit-tests/graphql-aspnet-subscriptions-tests/Web/WebSockets/WebSocketClientConnectionTests.cs index 22df65b25..04633dd75 100644 --- a/src/unit-tests/graphql-aspnet-subscriptions-tests/Web/WebSockets/WebSocketClientConnectionTests.cs +++ b/src/unit-tests/graphql-aspnet-subscriptions-tests/Web/WebSockets/WebSocketClientConnectionTests.cs @@ -30,7 +30,7 @@ public class WebSocketClientConnectionTests public async Task GeneralPropertyCheck() { var context = new DefaultHttpContext(); - context.Request.Headers.Add(SubscriptionConstants.WebSockets.WEBSOCKET_PROTOCOL_HEADER, "sub proto"); + context.Request.Headers.Append(SubscriptionConstants.WebSockets.WEBSOCKET_PROTOCOL_HEADER, "sub proto"); var provider = Substitute.For(); var securityContext = Substitute.For(); diff --git a/src/unit-tests/graphql-aspnet-subscriptions-tests/graphql-aspnet-subscriptions-tests.csproj b/src/unit-tests/graphql-aspnet-subscriptions-tests/graphql-aspnet-subscriptions-tests.csproj index a94ccac89..0619817b7 100644 --- a/src/unit-tests/graphql-aspnet-subscriptions-tests/graphql-aspnet-subscriptions-tests.csproj +++ b/src/unit-tests/graphql-aspnet-subscriptions-tests/graphql-aspnet-subscriptions-tests.csproj @@ -1,31 +1,11 @@  + - net7.0;net6.0; - latest - $(NoWarn);1701;1702;1705;1591;NU1603;RCS1021;IDE0060;IDE0052;IDE0044;IDE0059;IDE0052;IDE0017;IDE0039;TCS1089;RCS1090;RCS1118;SA1601;RCS1163 GraphQL.AspNet.Tests graphql-aspnet-subscriptions-tests - true - true - false - - ..\..\styles.ruleset - - - - - - - - - - - - - diff --git a/src/unit-tests/graphql-aspnet-testframework-tests/GlobalSuppressions.cs b/src/unit-tests/graphql-aspnet-testframework-tests/GlobalSuppressions.cs new file mode 100644 index 000000000..085832b85 --- /dev/null +++ b/src/unit-tests/graphql-aspnet-testframework-tests/GlobalSuppressions.cs @@ -0,0 +1,46 @@ +// ************************************************************* +// project: graphql-aspnet +// -- +// repo: https://github.com/graphql-aspnet +// docs: https://graphql-aspnet.github.io +// -- +// License: MIT +// ************************************************************* + +using System.Diagnostics.CodeAnalysis; + +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage( + "StyleCop.CSharp.DocumentationRules", + "SA1600:Elements should be documented", + Justification = "Documenting test methods is unwarranted at this time.", + Scope = "module")] + +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage( + "StyleCop.CSharp.DocumentationRules", + "SA1601:Elements should be documented", + Justification = "Documenting test methods is unwarranted at this time.", + Scope = "module")] + +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage( + "StyleCop.CSharp.NamingRules", + "SA1313:Parameter names should begin with lower-case letter", + Justification = "Testing of Alternative Naming schemas is required for unit tests", + Scope = "module")] + +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage( + "StyleCop.CSharp.NamingRules", + "SA1300:Element should begin with upper-case letter", + Justification = "Testing of Alternative Naming schemas is required for unit tests", + Scope = "module")] + +[assembly: SuppressMessage( + "Design", + "CA1065:Do not raise exceptions in unexpected locations", + Justification = "Test Project exceptions are for testing only", + Scope = "module")] + +[assembly: SuppressMessage( + "Design", + "CA1063:Implement IDisposable Correctly", + Justification = "Proper IDisposable not necessary for unit test project", + Scope = "module")] \ No newline at end of file diff --git a/src/unit-tests/graphql-aspnet-testframework-tests/TestServerBuilderTests.cs b/src/unit-tests/graphql-aspnet-testframework-tests/TestServerBuilderTests.cs new file mode 100644 index 000000000..ea427215a --- /dev/null +++ b/src/unit-tests/graphql-aspnet-testframework-tests/TestServerBuilderTests.cs @@ -0,0 +1,56 @@ +// ************************************************************* +// project: graphql-aspnet +// -- +// repo: https://github.com/graphql-aspnet +// docs: https://graphql-aspnet.github.io +// -- +// License: MIT +// ************************************************************* + +namespace GraphQL.AspNet.TestFramework.Tests +{ + using System.Threading.Tasks; + using GraphQL.AspNet.Attributes; + using GraphQL.AspNet.Controllers; + using GraphQL.AspNet.Schemas; + using GraphQL.AspNet.Tests.Framework; + using NUnit.Framework; + + [TestFixture] + public class TestServerBuilderTests + { + [Test] + public void ParallelBuildSameControllerTest() + { + Task.WaitAll( + Task.Run(BuildServer), + Task.Run(BuildServer), + Task.Run(BuildServer), + Task.Run(BuildServer), + Task.Run(BuildServer)); + } + + [GraphRoute("with-param")] + public class AppController : GraphController + { + [Query("get")] + public string Get(string id) + { + return id; + } + } + + private void BuildServer() + where T : GraphController + { + var builder = new TestServerBuilder(); + + builder.AddGraphQL(options => + { + options.AddController(); + }); + + builder.Build(); + } + } +} \ No newline at end of file diff --git a/src/unit-tests/graphql-aspnet-testframework-tests/graphql-aspnet-testframework-tests.csproj b/src/unit-tests/graphql-aspnet-testframework-tests/graphql-aspnet-testframework-tests.csproj new file mode 100644 index 000000000..6b68ab24b --- /dev/null +++ b/src/unit-tests/graphql-aspnet-testframework-tests/graphql-aspnet-testframework-tests.csproj @@ -0,0 +1,17 @@ + + + + + GraphQL.AspNet.TestFramework.Tests + graphql-aspnet-testframework-tests + + + + + + + + + + + diff --git a/src/unit-tests/graphql-aspnet-testframework/graphql-aspnet-testframework.csproj b/src/unit-tests/graphql-aspnet-testframework/graphql-aspnet-testframework.csproj index 614187d5b..31430a712 100644 --- a/src/unit-tests/graphql-aspnet-testframework/graphql-aspnet-testframework.csproj +++ b/src/unit-tests/graphql-aspnet-testframework/graphql-aspnet-testframework.csproj @@ -2,7 +2,7 @@ - net7.0;net6.0; + net9.0;net8.0; GraphQL.AspNet.Tests.Framework GraphQL.AspNet.TestFramework GraphQL ASP.NET Test Framework diff --git a/src/unit-tests/graphql-aspnet-tests-common/graphql-aspnet-tests-common.csproj b/src/unit-tests/graphql-aspnet-tests-common/graphql-aspnet-tests-common.csproj index debf0d78e..4d67b7524 100644 --- a/src/unit-tests/graphql-aspnet-tests-common/graphql-aspnet-tests-common.csproj +++ b/src/unit-tests/graphql-aspnet-tests-common/graphql-aspnet-tests-common.csproj @@ -1,31 +1,11 @@ + - net7.0;net6.0; - latest - $(NoWarn);1701;1702;1705;1591;NU1603;RCS1021;IDE0060;IDE0052;IDE0044;IDE0059;IDE0052;IDE0017;IDE0039;RCS1090;RCS1118;SA1601;RCS1163 GraphQL.AspNet.Tests.Common graphql-aspnet-tests-common - true - true - false - - ..\..\styles.ruleset - - - - - - - - - - - - - diff --git a/src/unit-tests/graphql-aspnet-tests-thirdpartydll/graphql-aspnet-tests-thirdpartydll.csproj b/src/unit-tests/graphql-aspnet-tests-thirdpartydll/graphql-aspnet-tests-thirdpartydll.csproj index 6455bd77d..ccb585eab 100644 --- a/src/unit-tests/graphql-aspnet-tests-thirdpartydll/graphql-aspnet-tests-thirdpartydll.csproj +++ b/src/unit-tests/graphql-aspnet-tests-thirdpartydll/graphql-aspnet-tests-thirdpartydll.csproj @@ -1,7 +1,7 @@  - net7.0;net6.0; + net9.0;net8.0; latest $(NoWarn);1701;1702;1705;1591;NU1603;IDE0060;IDE0052;IDE0044;IDE0059;IDE0052 GraphQL.AspNet.Tests.ThirdPartyDll diff --git a/src/unit-tests/graphql-aspnet-tests/Common/GraphIdTests.cs b/src/unit-tests/graphql-aspnet-tests/Common/GraphIdTests.cs index 1e7e8c9b6..25612b820 100644 --- a/src/unit-tests/graphql-aspnet-tests/Common/GraphIdTests.cs +++ b/src/unit-tests/graphql-aspnet-tests/Common/GraphIdTests.cs @@ -31,7 +31,7 @@ public void GeneralPropertyCheck() [Test] public void EmptyGraphIdCtor_IsEmptyString() { - GraphId id = new (); + GraphId id = default; Assert.IsNull(id.Value); } diff --git a/src/unit-tests/graphql-aspnet-tests/Common/OrderedDictionaryTests.cs b/src/unit-tests/graphql-aspnet-tests/Common/OrderedDictionaryTests.cs index ef8174de5..e56e4398d 100644 --- a/src/unit-tests/graphql-aspnet-tests/Common/OrderedDictionaryTests.cs +++ b/src/unit-tests/graphql-aspnet-tests/Common/OrderedDictionaryTests.cs @@ -16,13 +16,15 @@ namespace GraphQL.AspNet.Tests.Common using System.Linq; using GraphQL.AspNet.Common.Generics; using NUnit.Framework; + using OrderedDictionaryOfStringAndInt = GraphQL.AspNet.Common.Generics.OrderedDictionary; + using OrderedDictionaryOfStringAndString = GraphQL.AspNet.Common.Generics.OrderedDictionary; [TestFixture] public class OrderedDictionaryTests { - private OrderedDictionary GetAlphabetDictionary(IEqualityComparer comparer = null) + private OrderedDictionaryOfStringAndString GetAlphabetDictionary(IEqualityComparer comparer = null) { - OrderedDictionary alphabet = new OrderedDictionary(comparer); + OrderedDictionaryOfStringAndString alphabet = new OrderedDictionaryOfStringAndString(comparer); for (var a = Convert.ToInt32('a'); a <= Convert.ToInt32('z'); a++) { var c = Convert.ToChar(a); @@ -57,11 +59,11 @@ public int Compare(int x, int y) [Test] public void Constructor_FromOtherDictionary() { - var od = new OrderedDictionary(); + var od = new OrderedDictionaryOfStringAndString(); od.Add("key1", "value1"); od.Add("key2", "value2"); - var od2 = new OrderedDictionary(od); + var od2 = new OrderedDictionaryOfStringAndString(od); Assert.AreEqual(2, od2.Count); Assert.AreEqual("value1", od2["key1"]); Assert.AreEqual("value2", od2["key2"]); @@ -70,12 +72,12 @@ public void Constructor_FromOtherDictionary() [Test] public void Constructor_FromOtherDictionary_CaseInsensitive() { - var od = new OrderedDictionary(); + var od = new OrderedDictionaryOfStringAndString(); od.Add("key1", "value1"); od.Add("key2", "value2"); Assert.IsFalse(od.ContainsKey("KEY1")); - var od2 = new OrderedDictionary(od, StringComparer.InvariantCultureIgnoreCase); + var od2 = new OrderedDictionaryOfStringAndString(od, StringComparer.InvariantCultureIgnoreCase); Assert.AreEqual(2, od2.Count); Assert.AreEqual("value1", od2["KEY1"]); Assert.AreEqual("value2", od2["KEY2"]); @@ -84,7 +86,7 @@ public void Constructor_FromOtherDictionary_CaseInsensitive() [Test] public void Browser_PropertyCheck() { - var dic = new OrderedDictionary(); + var dic = new OrderedDictionaryOfStringAndInt(); dic.Add("key1", 1); dic.Add("key2", 2); @@ -110,7 +112,7 @@ public void Browser_PropertyCheck() [Test] public void DuplicateAdd_ThrowsError() { - var dic = new OrderedDictionary(); + var dic = new OrderedDictionaryOfStringAndInt(); dic.Add("key1", 1); Assert.Throws(() => { dic.Add("key1", 1); }); @@ -119,7 +121,7 @@ public void DuplicateAdd_ThrowsError() [Test] public void ICollectionKVP_CastingCheck() { - var dic = new OrderedDictionary(); + var dic = new OrderedDictionaryOfStringAndInt(); var kvp = new KeyValuePair("key1", 3); var col = dic as ICollection>; @@ -149,7 +151,7 @@ public void ICollectionKVP_CastingCheck() [Test] public void IDicionaryKVP_CastingCheck() { - var od = new OrderedDictionary() as IDictionary; + var od = new OrderedDictionaryOfStringAndString() as IDictionary; Assert.AreEqual(0, od.Count); od.Add("foo", "bar"); @@ -182,7 +184,7 @@ public void IDicionaryKVP_CastingCheck() [Test] public void IOrderedDictionary_CastingCheck() { - var od = new OrderedDictionary() as IOrderedDictionary; + var od = new OrderedDictionaryOfStringAndString() as IOrderedDictionary; Assert.AreEqual(0, od.Count); // IOrderedDictionary.Insert @@ -252,7 +254,7 @@ public void IDictionary_CastingCheck() [Test] public void Add() { - var od = new OrderedDictionary(); + var od = new OrderedDictionaryOfStringAndString(); Assert.AreEqual(0, od.Count); Assert.AreEqual(-1, od.IndexOf("foo")); @@ -268,7 +270,7 @@ public void Add() [Test] public void Remove() { - var od = new OrderedDictionary(); + var od = new OrderedDictionaryOfStringAndString(); od.Add("foo", "bar"); Assert.AreEqual(1, od.Count); @@ -280,7 +282,7 @@ public void Remove() [Test] public void RemoveAt() { - var od = new OrderedDictionary(); + var od = new OrderedDictionaryOfStringAndString(); od.Add("foo", "bar"); Assert.AreEqual(1, od.Count); @@ -585,7 +587,7 @@ public void DelegateKeyCollection_PropertyCheck() [Test] public void DictionaryEnumerator_Reset_YieldsFirstItemAgain() { - var dic = new OrderedDictionary(); + var dic = new OrderedDictionaryOfStringAndString(); dic.Add("a", "string1"); dic.Add("b", "string2"); @@ -604,7 +606,7 @@ public void DictionaryEnumerator_Reset_YieldsFirstItemAgain() [Test] public void DictionaryEnumerator_PropertyCheck() { - var dic = new OrderedDictionary(); + var dic = new OrderedDictionaryOfStringAndString(); dic.Add("a", "string1"); dic.Add("b", "string2"); diff --git a/src/unit-tests/graphql-aspnet-tests/Configuration/ConfigurationSetupTests.cs b/src/unit-tests/graphql-aspnet-tests/Configuration/ConfigurationSetupTests.cs index 5ae86eeea..397534745 100644 --- a/src/unit-tests/graphql-aspnet-tests/Configuration/ConfigurationSetupTests.cs +++ b/src/unit-tests/graphql-aspnet-tests/Configuration/ConfigurationSetupTests.cs @@ -9,6 +9,7 @@ namespace GraphQL.AspNet.Tests.Configuration { + using System; using System.Linq; using System.Reflection; using System.Threading.Tasks; @@ -323,5 +324,54 @@ public void ChangingGlobalConfig_ChangesHowControllersAreRegistered() Assert.AreEqual(ServiceLifetime.Singleton, descriptor.Lifetime); } + + [Test] + public void AddGraphQL_UseGraphQL_SameSchema() + { + var service1Collection = new ServiceCollection(); + var service2Collection = new ServiceCollection(); + + service1Collection.AddGraphQL(options => + { + options.AddGraphType(); + }); + service2Collection.AddGraphQL(options => + { + options.AddGraphType(); + }); + + var sp1 = service1Collection.BuildServiceProvider(); + var sp2 = service2Collection.BuildServiceProvider(); + sp1.UseGraphQL(); + sp2.UseGraphQL(); + + var schema1 = sp1.GetService(typeof(CandleSchema)) as ISchema; + var schema2 = sp2.GetService(typeof(CandleSchema)) as ISchema; + Assert.IsNotNull(schema1); + Assert.IsNotNull(schema2); + + Assert.IsTrue(schema1.KnownTypes.Contains(typeof(Candle))); + Assert.IsTrue(schema2.KnownTypes.Contains(typeof(Candle))); + } + + [Test] + public void AddGraphQL_AttemptingToInitializeSchemaASecondTime_ThrowsException() + { + var service1Collection = new ServiceCollection(); + var service2Collection = new ServiceCollection(); + + service1Collection.AddGraphQL(options => + { + options.AddGraphType(); + }); + + Assert.Throws(() => + { + service1Collection.AddGraphQL(options => + { + options.AddGraphType(); + }); + }); + } } } \ No newline at end of file diff --git a/src/unit-tests/graphql-aspnet-tests/Engine/TypeMakers/TestData/EnumWithDescriptions.cs b/src/unit-tests/graphql-aspnet-tests/Engine/TypeMakers/TestData/EnumWithDescriptionOnValues.cs similarity index 100% rename from src/unit-tests/graphql-aspnet-tests/Engine/TypeMakers/TestData/EnumWithDescriptions.cs rename to src/unit-tests/graphql-aspnet-tests/Engine/TypeMakers/TestData/EnumWithDescriptionOnValues.cs diff --git a/src/unit-tests/graphql-aspnet-tests/Execution/ControllerIsolationTests.cs b/src/unit-tests/graphql-aspnet-tests/Execution/ControllerIsolationTests.cs new file mode 100644 index 000000000..f8f0809c4 --- /dev/null +++ b/src/unit-tests/graphql-aspnet-tests/Execution/ControllerIsolationTests.cs @@ -0,0 +1,119 @@ +// ************************************************************* +// project: graphql-aspnet +// -- +// repo: https://github.com/graphql-aspnet +// docs: https://graphql-aspnet.github.io +// -- +// License: MIT +// ************************************************************* + +namespace GraphQL.AspNet.Tests.Execution +{ + using System.Threading.Tasks; + using GraphQL.AspNet.Configuration; + using GraphQL.AspNet.Tests.Execution.TestData.ControllerIsolationTestData; + using GraphQL.AspNet.Tests.Framework; + using GraphQL.AspNet.Tests.Framework.CommonHelpers; + using NUnit.Framework; + + [TestFixture] + public class ControllerIsolationTests + { + [Test] + public async Task IsolatedController_WithDownLevelInvocations_IsolatesCorrectly() + { + var query = @"query { + isolated { + extractInt + extractSecondInt + } + isolated2 { + extractInt + extractSecondInt + } + }"; + + var server = new TestServerBuilder() + .AddGraphQL(options => + { + options.AddController(); + options.AddController(); + options.ExecutionOptions.ResolverIsolation = ResolverIsolationOptions.All; + }) + + .Build(); + + var contextBuilder = server.CreateQueryContextBuilder(); + contextBuilder.AddQueryText(query); + + var result = await server.ExecuteQuery(contextBuilder); + Assert.AreEqual(0, result.Messages.Count); + + var jsonData = await server.RenderResult(contextBuilder); + + var expectedOutput = + @"{ + ""data"": { + ""isolated"" : { + ""extractInt"": 3, + ""extractSecondInt"": 4 + }, + ""isolated2"" : { + ""extractInt"": 3, + ""extractSecondInt"": 4 + } + } + }"; + + CommonAssertions.AreEqualJsonStrings(expectedOutput, jsonData); + } + + [Test] + public async Task IsolatedController_SameController_IsolatesCorrectly() + { + var query = @"query { + renamed: isolated { + extractInt + extractSecondInt + } + + isolated { + extractInt + extractSecondInt + } + }"; + + var server = new TestServerBuilder() + .AddGraphQL(options => + { + options.AddController(); + options.ExecutionOptions.ResolverIsolation = ResolverIsolationOptions.All; + }) + .Build(); + + var contextBuilder = server.CreateQueryContextBuilder(); + contextBuilder.AddQueryText(query); + + var result = await server.ExecuteQuery(contextBuilder); + Assert.AreEqual(0, result.Messages.Count); + + var jsonData = await server.RenderResult(contextBuilder); + + var expectedOutput = + @"{ + ""data"": { + ""isolated"" : { + ""extractInt"": 3, + ""extractSecondInt"": 4 + }, + ""renamed"" : { + ""extractInt"": 3, + ""extractSecondInt"": 4 + } + } + }"; + + CommonAssertions.AreEqualJsonStrings(expectedOutput, jsonData); + } + } +} \ No newline at end of file diff --git a/src/unit-tests/graphql-aspnet-tests/Execution/FieldExecutionOrderTests.cs b/src/unit-tests/graphql-aspnet-tests/Execution/FieldExecutionOrderTests.cs new file mode 100644 index 000000000..f74262620 --- /dev/null +++ b/src/unit-tests/graphql-aspnet-tests/Execution/FieldExecutionOrderTests.cs @@ -0,0 +1,107 @@ +// ************************************************************* +// project: graphql-aspnet +// -- +// repo: https://github.com/graphql-aspnet +// docs: https://graphql-aspnet.github.io +// -- +// License: MIT +// ************************************************************* + +namespace GraphQL.AspNet.Tests.Execution +{ + using System.Text.RegularExpressions; + using System.Threading.Tasks; + using GraphQL.AspNet.Tests.Execution.FieldExecutionorderTestData; + using GraphQL.AspNet.Tests.Framework; + using NUnit.Framework; + + [TestFixture] + public class FieldExecutionOrderTests + { + private static readonly Regex _whitespace = new Regex(@"\s+"); + + [Test] + public async Task OrderTest() + { + var server = new TestServerBuilder() + .AddGraphQL(o => + { + o.AddType(); + o.AddType(); + o.ResponseOptions.ExposeExceptions = true; + }) + .Build(); + + var builder = server.CreateQueryContextBuilder() + .AddQueryText("query { " + + " retrieveDefaultChickenMeal {" + + " description " + + " name " + + " id" + + " }" + + " hamburger { " + + " retrieveOtherHamburgerMeal {" + + " weight " + + " name " + + " id" + + " description " + + " }" + + " }" + + " chicken { " + + " retrieveOtherChickenMeal {" + + " id" + + " description " + + " name " + + " }" + + " }" + + " retrieveDefaultHamburgerMeal {" + + " id" + + " description " + + " name " + + " weight " + + " }" + + "}"); + + var expectedOutput = + @"{ + ""data"": { + ""retrieveDefaultChickenMeal"": { + ""description"": ""a top level chicken sandwich"", + ""name"": ""Chicken Bacon Ranch"", + ""id"": 5 + }, + ""hamburger"": { + ""retrieveOtherHamburgerMeal"": { + ""weight"": 1.3, + ""name"": ""Tiny Hamburger"", + ""id"": 5, + ""description"": ""a nested Hamburger"" + } + }, + ""chicken"": { + ""retrieveOtherChickenMeal"": { + ""id"": 5, + ""description"": ""a nested chicken sandwich"", + ""name"": ""Chicken Lettuce Tomato"" + } + }, + ""retrieveDefaultHamburgerMeal"": { + ""id"": 5, + ""description"": ""a top level hamburger"", + ""name"": ""Hamburger Supreme"", + ""weight"": 2.5 + } + } + }"; + + var result = await server.RenderResult(builder); + + // must be an exact string with the exact character order for the data + // dont use json matching just to be sure + result = _whitespace.Replace(result, string.Empty); + expectedOutput = _whitespace.Replace(expectedOutput, string.Empty); + + Assert.AreEqual(expectedOutput, result); + } + } +} \ No newline at end of file diff --git a/src/unit-tests/graphql-aspnet-tests/Execution/GeneralQueryExecutionTests3.cs b/src/unit-tests/graphql-aspnet-tests/Execution/GeneralQueryExecutionTests3.cs new file mode 100644 index 000000000..e14f2a47c --- /dev/null +++ b/src/unit-tests/graphql-aspnet-tests/Execution/GeneralQueryExecutionTests3.cs @@ -0,0 +1,94 @@ +// ************************************************************* +// project: graphql-aspnet +// -- +// repo: https://github.com/graphql-aspnet +// docs: https://graphql-aspnet.github.io +// -- +// License: MIT +// ************************************************************* + +namespace GraphQL.AspNet.Tests.Execution +{ + using System.Threading.Tasks; + using GraphQL.AspNet.Tests.Execution.TestData.ExecutionPlanTestData; + using GraphQL.AspNet.Tests.Framework; + using GraphQL.AspNet.Tests.Framework.CommonHelpers; + using NUnit.Framework; + + [TestFixture] + public class GeneralQueryExecutionTests3 + { + [Test] + public async Task Record_asInputObject_RendersObjectCorrectly() + { + var server = new TestServerBuilder() + .AddGraphQL(o => + { + o.AddType(); + }) + .Build(); + + // totalPeople exists on base controller + // totalEmployees exists on the added EmployeeController + var builder = server.CreateQueryContextBuilder() + .AddQueryText(@"query { + retrieveValue(record: {property1: 23}) + }"); + + var expectedOutput = + @"{ + ""data"": { + ""retrieveValue"" : 23 + } + }"; + + var result = await server.RenderResult(builder); + CommonAssertions.AreEqualJsonStrings(expectedOutput, result); + } + + [Test] + public async Task Self_Referencing_Nested_List_Input() + { + var server = new TestServerBuilder() + .AddGraphQL(o => + { + o.AddType(); + o.ResponseOptions.ExposeExceptions = true; + }) + .Build(); + + // totalPeople exists on base controller + // totalEmployees exists on the added EmployeeController + var builder = server.CreateQueryContextBuilder() + .AddQueryText(@"query { + countNestings(item: + { + name: ""root"", + children: [ + { + name: ""child1"", + children: [ + { name: ""child1.1""}, + {name: ""child1.2""} + ] + }, + {name: ""child2""} + ] + }) + }"); + + var expectedOutput = + @"{ + ""data"": { + ""countNestings"" : 5 + } + }"; + + var result = await server.ExecuteQuery(builder); + Assert.AreEqual(0, result.Messages.Count); + + var renderedResult = await server.RenderResult(builder); + CommonAssertions.AreEqualJsonStrings(expectedOutput, renderedResult); + } + } +} \ No newline at end of file diff --git a/src/unit-tests/graphql-aspnet-tests/Execution/GraphSkipSequencingTests.cs b/src/unit-tests/graphql-aspnet-tests/Execution/GraphSkipSequencingTests.cs new file mode 100644 index 000000000..0e832bf89 --- /dev/null +++ b/src/unit-tests/graphql-aspnet-tests/Execution/GraphSkipSequencingTests.cs @@ -0,0 +1,45 @@ +// ************************************************************* +// project: graphql-aspnet +// -- +// repo: https://github.com/graphql-aspnet +// docs: https://graphql-aspnet.github.io +// -- +// License: MIT +// ************************************************************* + +namespace GraphQL.AspNet.Tests.Execution +{ + using GraphQL.AspNet.Tests.Execution.TestData.GraphSkipSequencingTestData; + using GraphQL.AspNet.Tests.Framework; + using NUnit.Framework; + + [TestFixture] + public class GraphSkipSequencingTests + { + [Test] + public void SkipFieldWithSameNameAsAnotherField_OBJECT() + { + using var restore = new GraphQLGlobalRestorePoint(true); + + var server = new TestServerBuilder() + .AddGraphQL(o => + { + o.AddType(AspNet.Schemas.TypeSystem.TypeKind.OBJECT); + }) + .Build(); + } + + [Test] + public void SkipFieldWithSameNameAsAnotherField_INPUTOBJECT() + { + using var restore = new GraphQLGlobalRestorePoint(true); + + var server = new TestServerBuilder() + .AddGraphQL(o => + { + o.AddType(AspNet.Schemas.TypeSystem.TypeKind.INPUT_OBJECT); + }) + .Build(); + } + } +} diff --git a/src/unit-tests/graphql-aspnet-tests/Execution/IntrospectionTests.cs b/src/unit-tests/graphql-aspnet-tests/Execution/IntrospectionTests.cs index 0b999c12d..9054680f7 100644 --- a/src/unit-tests/graphql-aspnet-tests/Execution/IntrospectionTests.cs +++ b/src/unit-tests/graphql-aspnet-tests/Execution/IntrospectionTests.cs @@ -2013,5 +2013,191 @@ public async Task Descriptions_OnInheritedInterfaces_AreRetrievedViaIntrospectio CommonAssertions.AreEqualJsonStrings(expectedResponse, response); } + + [Test] + public async Task TypeExtension_ReturnsChildObject_WithCustomName_RendersWithCorrectCustomName() + { + var server = new TestServerBuilder() + .AddController() + .Build(); + + var context = server.CreateQueryContextBuilder() + .AddQueryText(@" + { + __type(name: ""TypeWithNoCustomName"") + { + kind + name + fields(includeDeprecated: true) { + name + } + } + }"); + + var response = await server.RenderResult(context); + + var expectedResult = @" + { + ""data"": { + ""__type"": { + ""kind"": ""OBJECT"", + ""name"": ""TypeWithNoCustomName"", + ""fields"": [ + { + ""name"": ""field1"" + }, + { + ""name"": ""fieldTwo"" + } + ] + } + } + }"; + + CommonAssertions.AreEqualJsonStrings(expectedResult, response); + + Assert.IsNotNull(server.Schema.KnownTypes.FindGraphType("TypeWithNoCustomName")); + Assert.IsNotNull(server.Schema.KnownTypes.FindGraphType("Type_With_Custom_Name")); + } + + [Test] + public async Task BatchTypeExntesion_ReturnsChildObject_WithCustomName_RendersWithCorrectCustomName() + { + var server = new TestServerBuilder() + .AddController() + .Build(); + + var context = server.CreateQueryContextBuilder() + .AddQueryText(@" + { + __type(name: ""TypeWithNoCustomName"") + { + kind + name + fields(includeDeprecated: true) { + name + } + } + }"); + + var response = await server.RenderResult(context); + + var expectedResult = @" + { + ""data"": { + ""__type"": { + ""kind"": ""OBJECT"", + ""name"": ""TypeWithNoCustomName"", + ""fields"": [ + { + ""name"": ""field1"" + }, + { + ""name"": ""fieldTwo"" + } + ] + } + } + }"; + + CommonAssertions.AreEqualJsonStrings(expectedResult, response); + + Assert.IsNotNull(server.Schema.KnownTypes.FindGraphType("TypeWithNoCustomName")); + Assert.IsNotNull(server.Schema.KnownTypes.FindGraphType("Type_With_Custom_Name")); + } + + [Test] + public async Task TypeExtension_Type_ThatHasCustomName_AndChildOfSameType_RendersProperly() + { + var server = new TestServerBuilder() + .AddController() + .Build(); + + var context = server.CreateQueryContextBuilder() + .AddQueryText(@" + { + #custom named + __type(name: ""Type_With_Custom_Name"") + { + kind + name + fields(includeDeprecated: true) { + name + } + } + }"); + + var response = await server.RenderResult(context); + + var expectedResult = @" + { + ""data"": { + ""__type"": { + ""kind"": ""OBJECT"", + ""name"": ""Type_With_Custom_Name"", + ""fields"": [ + { + ""name"": ""field1"" + }, + { + ""name"": ""fieldTwo"" + }, + { + ""name"": ""fieldThree"" + } + ] + } + } + }"; + + CommonAssertions.AreEqualJsonStrings(expectedResult, response); + } + + [Test] + public async Task BatchTypeExtension_Type_ThatHasCustomName_AndChildrenOfSameType_RendersProperly() + { + var server = new TestServerBuilder() + .AddController() + .Build(); + + var context = server.CreateQueryContextBuilder() + .AddQueryText(@" + { + #custom named + __type(name: ""Type_With_Custom_Name"") + { + kind + name + fields(includeDeprecated: true) { + name + } + } + }"); + + var response = await server.RenderResult(context); + + var expectedResult = @" + { + ""data"": { + ""__type"": { + ""kind"": ""OBJECT"", + ""name"": ""Type_With_Custom_Name"", + ""fields"": [ + { + ""name"": ""field1"" + }, + { + ""name"": ""fieldTwo"" + }, + { + ""name"": ""fieldThree"" + } + ] + } + } + }"; + + CommonAssertions.AreEqualJsonStrings(expectedResult, response); + } } } \ No newline at end of file diff --git a/src/unit-tests/graphql-aspnet-tests/Execution/RulesEngine/DocumentValidationRuleProcessorTests.cs b/src/unit-tests/graphql-aspnet-tests/Execution/RulesEngine/DocumentValidationRuleProcessorTests.cs index 782f66e7a..e504a517d 100644 --- a/src/unit-tests/graphql-aspnet-tests/Execution/RulesEngine/DocumentValidationRuleProcessorTests.cs +++ b/src/unit-tests/graphql-aspnet-tests/Execution/RulesEngine/DocumentValidationRuleProcessorTests.cs @@ -23,14 +23,24 @@ public class DocumentValidationRuleProcessorTests { public static readonly List QueriesToFail; public static readonly List QueriesToPass; + private static bool _rejectFurtherQueries; - private static void AddQueryFailure(string ruleNumberToBreak, string query) + private static void AddQueryFailure(string ruleNumberToBreak, string query, bool isolateAndStopOthers = false) { + if (_rejectFurtherQueries) + return; + + if (isolateAndStopOthers) + QueriesToFail.Clear(); + QueriesToFail.Add(new object[] { ruleNumberToBreak, query }); + _rejectFurtherQueries = _rejectFurtherQueries || isolateAndStopOthers; } private static void AddQuerySuccess(string relatedRule, string query) { + // relatedRule is not used added to make it clear what is being tested against + // when setting up tests QueriesToPass.Add(new object[] { query }); } @@ -154,6 +164,14 @@ fragment ElevFragment on Elevator { // inlined fragments must have a declared target type (invalid type bob) AddQueryFailure("5.5.1.2", "query Operation1{ peopleMover(id: 5) { ... on Bob { id } } } "); + // inlined fragments must have a declared target type of the correct case(invalid case on Escalator) + AddQueryFailure("5.5.1.2", "query Operation1{ peopleMover(id: 5) { ... on EscalaTor { id name } } } "); + + // inlined fragments must have a declared target type of the correct case(invalid case on Escalator) + // special test for multiple inline fragments that may result in a need to merge + // this should trigger on fragment type checking (5.5.1.2), not on fragment mergability (5.3.2) + AddQueryFailure("5.5.1.2", "query Operation1{ peopleMover(id: 5) { ... on Elevator {id name} ... on EscalaTor { id name } } } "); + // all declared target types of a fragment must be Union, interface or object on named fragment // frag 1 declars a target of the string scalar AddQueryFailure("5.5.1.3", "query Operation1{ peopleMovers { elevator(id: 5){ ...frag1 } } } fragment frag1 on String { unknownField1 }"); @@ -368,6 +386,8 @@ public void ExecuteRule_EnsureCorrectErrorIsGenerated(string expectedRuleError, var ruleBroke = message.MetaData["Rule"]; Assert.AreEqual(expectedRuleError, ruleBroke.ToString()); + if (_rejectFurtherQueries) + Assert.Inconclusive("Query executed in isolation. cannot determine status of others"); } [TestCaseSource(nameof(QueriesToPass))] diff --git a/src/unit-tests/graphql-aspnet-tests/Execution/SortedFieldExecutionContextListTests.cs b/src/unit-tests/graphql-aspnet-tests/Execution/SortedFieldExecutionContextListTests.cs deleted file mode 100644 index 197ca0827..000000000 --- a/src/unit-tests/graphql-aspnet-tests/Execution/SortedFieldExecutionContextListTests.cs +++ /dev/null @@ -1,82 +0,0 @@ -// ************************************************************* -// project: graphql-aspnet -// -- -// repo: https://github.com/graphql-aspnet -// docs: https://graphql-aspnet.github.io -// -- -// License: MIT -// ************************************************************* - -namespace GraphQL.AspNet.Tests.Execution -{ - using System; - using System.Linq; - using GraphQL.AspNet.Execution; - using GraphQL.AspNet.Execution.Contexts; - using GraphQL.AspNet.Interfaces.Execution; - using GraphQL.AspNet.Interfaces.Execution.Variables; - using NSubstitute; - using NUnit.Framework; - - [TestFixture] - public class SortedFieldExecutionContextListTests - { - private GraphFieldExecutionContext CreateFakeContext() - { - var parentContext = Substitute.For(); - parentContext.QueryRequest.Returns(Substitute.For()); - parentContext.ServiceProvider.Returns(Substitute.For()); - parentContext.Session.Returns(new QuerySession()); - - var request = Substitute.For(); - var variableData = Substitute.For(); - - return new GraphFieldExecutionContext(parentContext, request, variableData); - } - - [Test] - public void IsolatedContext_IsAddedToIsolatedList() - { - var list = new SortedFieldExecutionContextList(); - list.Add(this.CreateFakeContext(), true); - - Assert.AreEqual(1, list.IsolatedContexts.Count()); - Assert.AreEqual(0, list.ParalellContexts.Count()); - } - - [Test] - public void ParalellContext_IsAddedToParalellList() - { - var list = new SortedFieldExecutionContextList(); - list.Add(this.CreateFakeContext(), false); - - Assert.AreEqual(0, list.IsolatedContexts.Count()); - Assert.AreEqual(1, list.ParalellContexts.Count()); - } - - [Test] - public void ListOrderIsPreserved_WhenMultipleContextsAreAdded() - { - var list = new SortedFieldExecutionContextList(); - - var parentContext = Substitute.For(); - var request = Substitute.For(); - var variableData = Substitute.For(); - - var context1 = this.CreateFakeContext(); - var context2 = this.CreateFakeContext(); - var context3 = this.CreateFakeContext(); - var context4 = this.CreateFakeContext(); - - list.Add(context1, true); - list.Add(context2, false); - list.Add(context3, true); - list.Add(context4, false); - - Assert.AreEqual(context1, list.IsolatedContexts.ElementAt(0)); - Assert.AreEqual(context3, list.IsolatedContexts.ElementAt(1)); - Assert.AreEqual(context2, list.ParalellContexts.ElementAt(0)); - Assert.AreEqual(context4, list.ParalellContexts.ElementAt(1)); - } - } -} \ No newline at end of file diff --git a/src/unit-tests/graphql-aspnet-tests/Execution/TestData/ControllerIsolationTestData/Isolated2Controller.cs b/src/unit-tests/graphql-aspnet-tests/Execution/TestData/ControllerIsolationTestData/Isolated2Controller.cs new file mode 100644 index 000000000..f25a017f3 --- /dev/null +++ b/src/unit-tests/graphql-aspnet-tests/Execution/TestData/ControllerIsolationTestData/Isolated2Controller.cs @@ -0,0 +1,59 @@ +// ************************************************************* +// project: graphql-aspnet +// -- +// repo: https://github.com/graphql-aspnet +// docs: https://graphql-aspnet.github.io +// -- +// License: MIT +// ************************************************************* + +namespace GraphQL.AspNet.Tests.Execution.TestData.ControllerIsolationTestData +{ + using System; + using System.Threading; + using System.Threading.Tasks; + using GraphQL.AspNet.Attributes; + using GraphQL.AspNet.Controllers; + + public class Isolated2Controller : GraphController + { + private static SemaphoreSlim _slim = new SemaphoreSlim(1); + + [Query] + public async Task ExtractInt() + { + if (_slim.CurrentCount != 1) + throw new InvalidOperationException("Other Controller action in progress. Thrown from ExtractInt2."); + + try + { + await _slim.WaitAsync(); + await Task.Delay(15); + } + finally + { + _slim.Release(); + } + + return 3; + } + + [Query] + public async Task ExtractSecondInt() + { + if (_slim.CurrentCount != 1) + throw new InvalidOperationException("Other Controller action in progress. Thrown from ExtractSecondInt2."); + try + { + await _slim.WaitAsync(); + await Task.Delay(15); + } + finally + { + _slim.Release(); + } + + return 4; + } + } +} \ No newline at end of file diff --git a/src/unit-tests/graphql-aspnet-tests/Execution/TestData/ControllerIsolationTestData/IsolatedController.cs b/src/unit-tests/graphql-aspnet-tests/Execution/TestData/ControllerIsolationTestData/IsolatedController.cs new file mode 100644 index 000000000..f1c77d607 --- /dev/null +++ b/src/unit-tests/graphql-aspnet-tests/Execution/TestData/ControllerIsolationTestData/IsolatedController.cs @@ -0,0 +1,59 @@ +// ************************************************************* +// project: graphql-aspnet +// -- +// repo: https://github.com/graphql-aspnet +// docs: https://graphql-aspnet.github.io +// -- +// License: MIT +// ************************************************************* + +namespace GraphQL.AspNet.Tests.Execution.TestData.ControllerIsolationTestData +{ + using System; + using System.Threading; + using System.Threading.Tasks; + using GraphQL.AspNet.Attributes; + using GraphQL.AspNet.Controllers; + + public class IsolatedController : GraphController + { + private static SemaphoreSlim _slim = new SemaphoreSlim(1); + + [Query] + public async Task ExtractInt() + { + if (_slim.CurrentCount != 1) + throw new InvalidOperationException("Other Controller action in progress. Thrown from ExtractInt. " + this.Context.Request.Origin.Path); + + try + { + await _slim.WaitAsync(); + await Task.Delay(15); + } + finally + { + _slim.Release(); + } + + return 3; + } + + [Query] + public async Task ExtractSecondInt() + { + if (_slim.CurrentCount != 1) + throw new InvalidOperationException("Other Controller action in progress. Thrown from ExtractSecondInt." + this.Context.Request.Origin.Path); + try + { + await _slim.WaitAsync(); + await Task.Delay(15); + } + finally + { + _slim.Release(); + } + + return 4; + } + } +} \ No newline at end of file diff --git a/src/unit-tests/graphql-aspnet-tests/Execution/TestData/ExecutionPlanTestData/MyRecord.cs b/src/unit-tests/graphql-aspnet-tests/Execution/TestData/ExecutionPlanTestData/MyRecord.cs new file mode 100644 index 000000000..09ae31511 --- /dev/null +++ b/src/unit-tests/graphql-aspnet-tests/Execution/TestData/ExecutionPlanTestData/MyRecord.cs @@ -0,0 +1,16 @@ +// ************************************************************* +// project: graphql-aspnet +// -- +// repo: https://github.com/graphql-aspnet +// docs: https://graphql-aspnet.github.io +// -- +// License: MIT +// ************************************************************* + +namespace GraphQL.AspNet.Tests.Execution.TestData.ExecutionPlanTestData +{ + public record MyRecord + { + public int Property1 { get; set; } + } +} \ No newline at end of file diff --git a/src/unit-tests/graphql-aspnet-tests/Execution/TestData/ExecutionPlanTestData/RecordAsInputObjectController.cs b/src/unit-tests/graphql-aspnet-tests/Execution/TestData/ExecutionPlanTestData/RecordAsInputObjectController.cs new file mode 100644 index 000000000..aa1228d46 --- /dev/null +++ b/src/unit-tests/graphql-aspnet-tests/Execution/TestData/ExecutionPlanTestData/RecordAsInputObjectController.cs @@ -0,0 +1,23 @@ +// ************************************************************* +// project: graphql-aspnet +// -- +// repo: https://github.com/graphql-aspnet +// docs: https://graphql-aspnet.github.io +// -- +// License: MIT +// ************************************************************* + +namespace GraphQL.AspNet.Tests.Execution.TestData.ExecutionPlanTestData +{ + using GraphQL.AspNet.Attributes; + using GraphQL.AspNet.Controllers; + + public class RecordAsInputObjectController : GraphController + { + [QueryRoot] + public int RetrieveValue(MyRecord record) + { + return record.Property1; + } + } +} \ No newline at end of file diff --git a/src/unit-tests/graphql-aspnet-tests/Execution/TestData/ExecutionPlanTestData/SelfReferencingInputObject.cs b/src/unit-tests/graphql-aspnet-tests/Execution/TestData/ExecutionPlanTestData/SelfReferencingInputObject.cs new file mode 100644 index 000000000..13c707eb1 --- /dev/null +++ b/src/unit-tests/graphql-aspnet-tests/Execution/TestData/ExecutionPlanTestData/SelfReferencingInputObject.cs @@ -0,0 +1,20 @@ +// ************************************************************* +// project: graphql-aspnet +// -- +// repo: https://github.com/graphql-aspnet +// docs: https://graphql-aspnet.github.io +// -- +// License: MIT +// ************************************************************* + +namespace GraphQL.AspNet.Tests.Execution.TestData.ExecutionPlanTestData +{ + using System.Collections.Generic; + + public class SelfReferencingInputObject + { + public string Name { get; set; } + + public IEnumerable Children { get; set; } + } +} diff --git a/src/unit-tests/graphql-aspnet-tests/Execution/TestData/ExecutionPlanTestData/SelfReferencingInputObjectController.cs b/src/unit-tests/graphql-aspnet-tests/Execution/TestData/ExecutionPlanTestData/SelfReferencingInputObjectController.cs new file mode 100644 index 000000000..7663a5601 --- /dev/null +++ b/src/unit-tests/graphql-aspnet-tests/Execution/TestData/ExecutionPlanTestData/SelfReferencingInputObjectController.cs @@ -0,0 +1,38 @@ +// ************************************************************* +// project: graphql-aspnet +// -- +// repo: https://github.com/graphql-aspnet +// docs: https://graphql-aspnet.github.io +// -- +// License: MIT +// ************************************************************* + +namespace GraphQL.AspNet.Tests.Execution.TestData.ExecutionPlanTestData +{ + using System.Collections.Generic; + using GraphQL.AspNet.Attributes; + using GraphQL.AspNet.Controllers; + + public class SelfReferencingInputObjectController : GraphController + { + [QueryRoot] + public int CountNestings(SelfReferencingInputObject item) + { + var i = 0; + var stack = new Stack(); + stack.Push(item); + while (stack.Count > 0) + { + var curItem = stack.Pop(); + i++; + if (curItem.Children != null) + { + foreach (var child in curItem.Children) + stack.Push(child); + } + } + + return i; + } + } +} diff --git a/src/unit-tests/graphql-aspnet-tests/Execution/TestData/FieldExecutionorderTestData/ChickenController.cs b/src/unit-tests/graphql-aspnet-tests/Execution/TestData/FieldExecutionorderTestData/ChickenController.cs new file mode 100644 index 000000000..e5d05fec6 --- /dev/null +++ b/src/unit-tests/graphql-aspnet-tests/Execution/TestData/FieldExecutionorderTestData/ChickenController.cs @@ -0,0 +1,44 @@ +// ************************************************************* +// project: graphql-aspnet +// -- +// repo: https://github.com/graphql-aspnet +// docs: https://graphql-aspnet.github.io +// -- +// License: MIT +// ************************************************************* + +namespace GraphQL.AspNet.Tests.Execution.FieldExecutionorderTestData +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + using GraphQL.AspNet.Attributes; + using GraphQL.AspNet.Controllers; + + public class ChickenController : GraphController + { + [QueryRoot] + public ChickenMeal RetrieveDefaultChickenMeal() + { + return new ChickenMeal() + { + Id = 5, + Name = "Chicken Bacon Ranch", + Description = "a top level chicken sandwich", + }; + } + + [Query] + public ChickenMeal RetrieveOtherChickenMeal() + { + return new ChickenMeal() + { + Id = 5, + Name = "Chicken Lettuce Tomato", + Description = "a nested chicken sandwich", + }; + } + } +} \ No newline at end of file diff --git a/src/unit-tests/graphql-aspnet-tests/Execution/TestData/FieldExecutionorderTestData/ChickenMeal.cs b/src/unit-tests/graphql-aspnet-tests/Execution/TestData/FieldExecutionorderTestData/ChickenMeal.cs new file mode 100644 index 000000000..4f308afb2 --- /dev/null +++ b/src/unit-tests/graphql-aspnet-tests/Execution/TestData/FieldExecutionorderTestData/ChickenMeal.cs @@ -0,0 +1,26 @@ +// ************************************************************* +// project: graphql-aspnet +// -- +// repo: https://github.com/graphql-aspnet +// docs: https://graphql-aspnet.github.io +// -- +// License: MIT +// ************************************************************* + +namespace GraphQL.AspNet.Tests.Execution.FieldExecutionorderTestData +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + + public class ChickenMeal + { + public int Id { get; set; } + + public string Name { get; set; } + + public string Description { get; set; } + } +} \ No newline at end of file diff --git a/src/unit-tests/graphql-aspnet-tests/Execution/TestData/FieldExecutionorderTestData/HamburgerController.cs b/src/unit-tests/graphql-aspnet-tests/Execution/TestData/FieldExecutionorderTestData/HamburgerController.cs new file mode 100644 index 000000000..b4992bf01 --- /dev/null +++ b/src/unit-tests/graphql-aspnet-tests/Execution/TestData/FieldExecutionorderTestData/HamburgerController.cs @@ -0,0 +1,41 @@ +// ************************************************************* +// project: graphql-aspnet +// -- +// repo: https://github.com/graphql-aspnet +// docs: https://graphql-aspnet.github.io +// -- +// License: MIT +// ************************************************************* + +namespace GraphQL.AspNet.Tests.Execution.FieldExecutionorderTestData +{ + using GraphQL.AspNet.Attributes; + using GraphQL.AspNet.Controllers; + + public class HamburgerController : GraphController + { + [QueryRoot] + public HamburgerMeal RetrieveDefaultHamburgerMeal() + { + return new HamburgerMeal() + { + Id = 5, + Name = "Hamburger Supreme", + Weight = 2.5f, + Description = "a top level hamburger", + }; + } + + [Query] + public HamburgerMeal RetrieveOtherHamburgerMeal() + { + return new HamburgerMeal() + { + Id = 5, + Name = "Tiny Hamburger", + Weight = 1.3f, + Description = "a nested Hamburger", + }; + } + } +} \ No newline at end of file diff --git a/src/unit-tests/graphql-aspnet-tests/Execution/TestData/FieldExecutionorderTestData/HamburgerMeal.cs b/src/unit-tests/graphql-aspnet-tests/Execution/TestData/FieldExecutionorderTestData/HamburgerMeal.cs new file mode 100644 index 000000000..839d9fc8d --- /dev/null +++ b/src/unit-tests/graphql-aspnet-tests/Execution/TestData/FieldExecutionorderTestData/HamburgerMeal.cs @@ -0,0 +1,28 @@ +// ************************************************************* +// project: graphql-aspnet +// -- +// repo: https://github.com/graphql-aspnet +// docs: https://graphql-aspnet.github.io +// -- +// License: MIT +// ************************************************************* + +namespace GraphQL.AspNet.Tests.Execution.FieldExecutionorderTestData +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + + public class HamburgerMeal + { + public int Id { get; set; } + + public string Name { get; set; } + + public float Weight { get; set; } + + public string Description { get; set; } + } +} \ No newline at end of file diff --git a/src/unit-tests/graphql-aspnet-tests/Execution/TestData/GraphSkipSequencingTestData/ClassWithRenamedField.cs b/src/unit-tests/graphql-aspnet-tests/Execution/TestData/GraphSkipSequencingTestData/ClassWithRenamedField.cs new file mode 100644 index 000000000..76e05af30 --- /dev/null +++ b/src/unit-tests/graphql-aspnet-tests/Execution/TestData/GraphSkipSequencingTestData/ClassWithRenamedField.cs @@ -0,0 +1,22 @@ +// ************************************************************* +// project: graphql-aspnet +// -- +// repo: https://github.com/graphql-aspnet +// docs: https://graphql-aspnet.github.io +// -- +// License: MIT +// ************************************************************* + +namespace GraphQL.AspNet.Tests.Execution.TestData.GraphSkipSequencingTestData +{ + using GraphQL.AspNet.Attributes; + + public class ClassWithRenamedField + { + [GraphSkip] + public int RootNamedField { get; set; } + + [GraphField("RootNamedField")] + public string ReNamedField { get; set; } + } +} diff --git a/src/unit-tests/graphql-aspnet-tests/Execution/TestData/IntrospectionTestData/TypeWithCustomName.cs b/src/unit-tests/graphql-aspnet-tests/Execution/TestData/IntrospectionTestData/TypeWithCustomName.cs new file mode 100644 index 000000000..29ceab186 --- /dev/null +++ b/src/unit-tests/graphql-aspnet-tests/Execution/TestData/IntrospectionTestData/TypeWithCustomName.cs @@ -0,0 +1,22 @@ +// ************************************************************* +// project: graphql-aspnet +// -- +// repo: https://github.com/graphql-aspnet +// docs: https://graphql-aspnet.github.io +// -- +// License: MIT +// ************************************************************* + +namespace GraphQL.AspNet.Tests.Execution.TestData.IntrospectionTestData +{ + using GraphQL.AspNet.Attributes; + + [GraphType("Type_With_Custom_Name")] + public class TypeWithCustomName + { + public int Field1 { get; set; } + + [GraphField("FieldTwo")] + public string Field2 { get; set; } + } +} \ No newline at end of file diff --git a/src/unit-tests/graphql-aspnet-tests/Execution/TestData/IntrospectionTestData/TypeWithCustomNameBatchExtensionController.cs b/src/unit-tests/graphql-aspnet-tests/Execution/TestData/IntrospectionTestData/TypeWithCustomNameBatchExtensionController.cs new file mode 100644 index 000000000..60ef74545 --- /dev/null +++ b/src/unit-tests/graphql-aspnet-tests/Execution/TestData/IntrospectionTestData/TypeWithCustomNameBatchExtensionController.cs @@ -0,0 +1,45 @@ +// ************************************************************* +// project: graphql-aspnet +// -- +// repo: https://github.com/graphql-aspnet +// docs: https://graphql-aspnet.github.io +// -- +// License: MIT +// ************************************************************* + +namespace GraphQL.AspNet.Tests.Execution.TestData.IntrospectionTestData +{ + using System.Collections; + using System.Collections.Generic; + using GraphQL.AspNet.Attributes; + using GraphQL.AspNet.Controllers; + + public class TypeWithCustomNameBatchExtensionController : GraphController + { + [BatchTypeExtension(typeof(TypeWithCustomName), "fieldThree")] + public IDictionary> TypeWithCustomNameField3( + IEnumerable parents) + { + var dic = new Dictionary>(); + var i = 0; + foreach (var item in parents) + { + dic.Add(item, new List() + { + new TypeWithCustomName() + { + Field1 = i++, + Field2 = $"Child_Of_{item.Field2}", + }, + new TypeWithCustomName() + { + Field1 = i++, + Field2 = $"Child_Of_{item.Field2}", + }, + }); + } + + return dic; + } + } +} \ No newline at end of file diff --git a/src/unit-tests/graphql-aspnet-tests/Execution/TestData/IntrospectionTestData/TypeWithCustomNameTypeExtensionController.cs b/src/unit-tests/graphql-aspnet-tests/Execution/TestData/IntrospectionTestData/TypeWithCustomNameTypeExtensionController.cs new file mode 100644 index 000000000..e2f8b7bfb --- /dev/null +++ b/src/unit-tests/graphql-aspnet-tests/Execution/TestData/IntrospectionTestData/TypeWithCustomNameTypeExtensionController.cs @@ -0,0 +1,28 @@ +// ************************************************************* +// project: graphql-aspnet +// -- +// repo: https://github.com/graphql-aspnet +// docs: https://graphql-aspnet.github.io +// -- +// License: MIT +// ************************************************************* + +namespace GraphQL.AspNet.Tests.Execution.TestData.IntrospectionTestData +{ + using GraphQL.AspNet.Attributes; + using GraphQL.AspNet.Controllers; + + public class TypeWithCustomNameTypeExtensionController : GraphController + { + [TypeExtension(typeof(TypeWithCustomName), "fieldThree")] + public TypeWithCustomName TypeWithCustomNameField3( + TypeWithCustomName parent) + { + return new TypeWithCustomName() + { + Field1 = 0, + Field2 = $"Child_Of_{parent.Field2}", + }; + } + } +} \ No newline at end of file diff --git a/src/unit-tests/graphql-aspnet-tests/Execution/TestData/IntrospectionTestData/TypeWithNoCustomName.cs b/src/unit-tests/graphql-aspnet-tests/Execution/TestData/IntrospectionTestData/TypeWithNoCustomName.cs new file mode 100644 index 000000000..24a823b23 --- /dev/null +++ b/src/unit-tests/graphql-aspnet-tests/Execution/TestData/IntrospectionTestData/TypeWithNoCustomName.cs @@ -0,0 +1,16 @@ +// ************************************************************* +// project: graphql-aspnet +// -- +// repo: https://github.com/graphql-aspnet +// docs: https://graphql-aspnet.github.io +// -- +// License: MIT +// ************************************************************* + +namespace GraphQL.AspNet.Tests.Execution.TestData.IntrospectionTestData +{ + public class TypeWithNoCustomName + { + public string Field1 { get; set; } + } +} \ No newline at end of file diff --git a/src/unit-tests/graphql-aspnet-tests/Execution/TestData/IntrospectionTestData/TypeWithNoCustomNameBatchExtensionController.cs b/src/unit-tests/graphql-aspnet-tests/Execution/TestData/IntrospectionTestData/TypeWithNoCustomNameBatchExtensionController.cs new file mode 100644 index 000000000..3fc5c4a7e --- /dev/null +++ b/src/unit-tests/graphql-aspnet-tests/Execution/TestData/IntrospectionTestData/TypeWithNoCustomNameBatchExtensionController.cs @@ -0,0 +1,47 @@ +// ************************************************************* +// project: graphql-aspnet +// -- +// repo: https://github.com/graphql-aspnet +// docs: https://graphql-aspnet.github.io +// -- +// License: MIT +// ************************************************************* + +namespace GraphQL.AspNet.Tests.Execution.TestData.IntrospectionTestData +{ + using System.Collections; + using System.Collections.Generic; + using GraphQL.AspNet.Attributes; + using GraphQL.AspNet.Controllers; + + public class TypeWithNoCustomNameBatchExtensionController : GraphController + { + [QueryRoot] + public TypeWithNoCustomName FetchItem() + { + return new TypeWithNoCustomName() + { + Field1 = "Data", + }; + } + + [BatchTypeExtension(typeof(TypeWithNoCustomName), "fieldTwo")] + public IDictionary TypeWithNoCustomNameField2( + IEnumerable parents) + { + var dic = new Dictionary(); + + var i = 0; + foreach (var item in parents) + { + dic.Add(item, new TypeWithCustomName() + { + Field1 = i++, + Field2 = $"Child_Of_{item.Field1}", + }); + } + + return dic; + } + } +} \ No newline at end of file diff --git a/src/unit-tests/graphql-aspnet-tests/Execution/TestData/IntrospectionTestData/TypeWithNoCustomNameTypeExtensionController.cs b/src/unit-tests/graphql-aspnet-tests/Execution/TestData/IntrospectionTestData/TypeWithNoCustomNameTypeExtensionController.cs new file mode 100644 index 000000000..77020a0e9 --- /dev/null +++ b/src/unit-tests/graphql-aspnet-tests/Execution/TestData/IntrospectionTestData/TypeWithNoCustomNameTypeExtensionController.cs @@ -0,0 +1,37 @@ +// ************************************************************* +// project: graphql-aspnet +// -- +// repo: https://github.com/graphql-aspnet +// docs: https://graphql-aspnet.github.io +// -- +// License: MIT +// ************************************************************* + +namespace GraphQL.AspNet.Tests.Execution.TestData.IntrospectionTestData +{ + using GraphQL.AspNet.Attributes; + using GraphQL.AspNet.Controllers; + + public class TypeWithNoCustomNameTypeExtensionController : GraphController + { + [QueryRoot] + public TypeWithNoCustomName FetchItem() + { + return new TypeWithNoCustomName() + { + Field1 = "Data", + }; + } + + [TypeExtension(typeof(TypeWithNoCustomName), "fieldTwo")] + public TypeWithCustomName TypeWithNoCustomNameField2( + TypeWithNoCustomName parent) + { + return new TypeWithCustomName() + { + Field1 = 1, + Field2 = $"Child_Of_{parent.Field1}", + }; + } + } +} \ No newline at end of file diff --git a/src/unit-tests/graphql-aspnet-tests/GlobalSuppressions.cs b/src/unit-tests/graphql-aspnet-tests/GlobalSuppressions.cs index 23073b202..085832b85 100644 --- a/src/unit-tests/graphql-aspnet-tests/GlobalSuppressions.cs +++ b/src/unit-tests/graphql-aspnet-tests/GlobalSuppressions.cs @@ -15,6 +15,12 @@ Justification = "Documenting test methods is unwarranted at this time.", Scope = "module")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage( + "StyleCop.CSharp.DocumentationRules", + "SA1601:Elements should be documented", + Justification = "Documenting test methods is unwarranted at this time.", + Scope = "module")] + [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage( "StyleCop.CSharp.NamingRules", "SA1313:Parameter names should begin with lower-case letter", diff --git a/src/unit-tests/graphql-aspnet-tests/Internal/Templating/ControllerTemplateTests.cs b/src/unit-tests/graphql-aspnet-tests/Internal/Templating/ControllerTemplateTests.cs index afc9c4f04..f4a87fb91 100644 --- a/src/unit-tests/graphql-aspnet-tests/Internal/Templating/ControllerTemplateTests.cs +++ b/src/unit-tests/graphql-aspnet-tests/Internal/Templating/ControllerTemplateTests.cs @@ -124,5 +124,19 @@ public void Parse_InheritedAction_IsIncludedInTheTemplate() Assert.IsNotNull(template.Actions.Single(x => x.Name.EndsWith(nameof(BaseControllerWithAction.BaseControllerAction)))); Assert.IsNotNull(template.Actions.Single(x => x.Name.EndsWith(nameof(ControllerWithInheritedAction.ChildControllerAction)))); } + + [Test] + public void Parse_BatchExtensionWithCustomNamedObject_HasAppropriateTypeExpression() + { + var template = new GraphControllerTemplate(typeof(ControllerWithActionAsTypeExtensionForCustomNamedObject)); + template.Parse(); + template.ValidateOrThrow(); + + var field = template.FieldTemplates[0]; + + // type expression should be the custom name on the type + // not the default name + Assert.AreEqual("[Custom_Named_Object]", field.TypeExpression.ToString()); + } } } \ No newline at end of file diff --git a/src/unit-tests/graphql-aspnet-tests/Internal/Templating/ControllerTestData/ControllerWithActionAsTypeExtensionForCustomNamedObject.cs b/src/unit-tests/graphql-aspnet-tests/Internal/Templating/ControllerTestData/ControllerWithActionAsTypeExtensionForCustomNamedObject.cs new file mode 100644 index 000000000..7ff8103da --- /dev/null +++ b/src/unit-tests/graphql-aspnet-tests/Internal/Templating/ControllerTestData/ControllerWithActionAsTypeExtensionForCustomNamedObject.cs @@ -0,0 +1,26 @@ +// ************************************************************* +// project: graphql-aspnet +// -- +// repo: https://github.com/graphql-aspnet +// docs: https://graphql-aspnet.github.io +// -- +// License: MIT +// ************************************************************* + +namespace GraphQL.AspNet.Tests.Internal.Templating.ControllerTestData +{ + using System.Collections.Generic; + using GraphQL.AspNet.Attributes; + using GraphQL.AspNet.Controllers; + using GraphQL.AspNet.Tests.Internal.Templating.ExtensionMethodTestData; + + public class ControllerWithActionAsTypeExtensionForCustomNamedObject : GraphController + { + [BatchTypeExtension(typeof(CustomNamedObject), "fieldThree")] + public IDictionary> ObjExtension( + IEnumerable customNamedObjects) + { + return null; + } + } +} \ No newline at end of file diff --git a/src/unit-tests/graphql-aspnet-tests/Internal/Templating/EnumTestData/EnumWithDescriptions.cs b/src/unit-tests/graphql-aspnet-tests/Internal/Templating/EnumTestData/EnumWithDescriptionOnValues.cs similarity index 100% rename from src/unit-tests/graphql-aspnet-tests/Internal/Templating/EnumTestData/EnumWithDescriptions.cs rename to src/unit-tests/graphql-aspnet-tests/Internal/Templating/EnumTestData/EnumWithDescriptionOnValues.cs diff --git a/src/unit-tests/graphql-aspnet-tests/Internal/Templating/ExtensionMethodTestData/CustomNamedObject.cs b/src/unit-tests/graphql-aspnet-tests/Internal/Templating/ExtensionMethodTestData/CustomNamedObject.cs new file mode 100644 index 000000000..5f75fcffd --- /dev/null +++ b/src/unit-tests/graphql-aspnet-tests/Internal/Templating/ExtensionMethodTestData/CustomNamedObject.cs @@ -0,0 +1,19 @@ +// ************************************************************* +// project: graphql-aspnet +// -- +// repo: https://github.com/graphql-aspnet +// docs: https://graphql-aspnet.github.io +// -- +// License: MIT +// ************************************************************* + +namespace GraphQL.AspNet.Tests.Internal.Templating.ExtensionMethodTestData +{ + using GraphQL.AspNet.Attributes; + + [GraphType("Custom_Named_Object")] + public class CustomNamedObject + { + public string FieldOne { get; set; } + } +} \ No newline at end of file diff --git a/src/unit-tests/graphql-aspnet-tests/Internal/Templating/ExtensionMethodTestData/ExtensionMethodController.cs b/src/unit-tests/graphql-aspnet-tests/Internal/Templating/ExtensionMethodTestData/ExtensionMethodController.cs index ce647befa..00fa1ac87 100644 --- a/src/unit-tests/graphql-aspnet-tests/Internal/Templating/ExtensionMethodTestData/ExtensionMethodController.cs +++ b/src/unit-tests/graphql-aspnet-tests/Internal/Templating/ExtensionMethodTestData/ExtensionMethodController.cs @@ -67,6 +67,24 @@ public IGraphActionResult InterfaceBatchTestExtension(IEnumerable items) + { + return null; + } + + [BatchTypeExtension(typeof(CustomNamedObject), "fieldThree", typeof(CustomNamedObject))] + public IGraphActionResult Batch_ChildIsSameCustomNamedObjectTestExtension(IEnumerable items) + { + return null; + } + [BatchTypeExtension(typeof(TwoPropertyObject), "Property3")] public IDictionary> CustomValidReturnType(IEnumerable sourceData, int arg1) { diff --git a/src/unit-tests/graphql-aspnet-tests/Internal/Templating/InputObjectTemplateTests.cs b/src/unit-tests/graphql-aspnet-tests/Internal/Templating/InputObjectTemplateTests.cs index 097cf2fa7..c9f681290 100644 --- a/src/unit-tests/graphql-aspnet-tests/Internal/Templating/InputObjectTemplateTests.cs +++ b/src/unit-tests/graphql-aspnet-tests/Internal/Templating/InputObjectTemplateTests.cs @@ -374,5 +374,23 @@ public void Parse_DuplicatePropertyPaths_ThrowsException() template.ValidateOrThrow(); }); } + + [Test] + public void Parse_OnRecord_YieldsTemplate() + { + var template = new InputObjectGraphTypeTemplate(typeof(InputRecord)); + template.Parse(); + template.ValidateOrThrow(); + + Assert.AreEqual(2, template.FieldTemplates.Count); + var field1 = template.FieldTemplates.SingleOrDefault(x => x.Value.Name == "Property1").Value; + var field2 = template.FieldTemplates.SingleOrDefault(x => x.Value.Name == "Property2").Value; + + Assert.IsNotNull(field1); + Assert.IsNotNull(field2); + + Assert.AreEqual(typeof(int), field1.ObjectType); + Assert.AreEqual(typeof(string), field2.ObjectType); + } } } \ No newline at end of file diff --git a/src/unit-tests/graphql-aspnet-tests/Internal/Templating/ObjectGraphTypeTemplateTests.cs b/src/unit-tests/graphql-aspnet-tests/Internal/Templating/ObjectGraphTypeTemplateTests.cs index 026c11572..f0576555e 100644 --- a/src/unit-tests/graphql-aspnet-tests/Internal/Templating/ObjectGraphTypeTemplateTests.cs +++ b/src/unit-tests/graphql-aspnet-tests/Internal/Templating/ObjectGraphTypeTemplateTests.cs @@ -431,5 +431,34 @@ public void Parse_InternalInheritedMembers_AreNotTemplated() Assert.AreEqual("Method3", fieldTemplate0.Name); Assert.AreEqual("Field1", fieldTemplate1.Name); } + + [Test] + public void Parse_Struct_WithGraphTypeNameOverride_ParsesCorrectly() + { + var template = new ObjectGraphTypeTemplate(typeof(SimpleScalarStructWithTypeOverride)); + template.Parse(); + template.ValidateOrThrow(); + + Assert.IsNotNull(template); + Assert.AreEqual("SomeTypeName", template.Name); + } + + [Test] + public void Parse_Record_ParsesCorrectly() + { + var template = new ObjectGraphTypeTemplate(typeof(ObjectRecord)); + template.Parse(); + template.ValidateOrThrow(); + + Assert.AreEqual(2, template.FieldTemplates.Count); + var field1 = template.FieldTemplates.SingleOrDefault(x => x.Name == "Property1"); + var field2 = template.FieldTemplates.SingleOrDefault(x => x.Name == "Property2"); + + Assert.IsNotNull(field1); + Assert.IsNotNull(field2); + + Assert.AreEqual(typeof(int), field1.ObjectType); + Assert.AreEqual(typeof(string), field2.ObjectType); + } } } \ No newline at end of file diff --git a/src/unit-tests/graphql-aspnet-tests/Internal/Templating/ObjectTypeTests/InputRecord.cs b/src/unit-tests/graphql-aspnet-tests/Internal/Templating/ObjectTypeTests/InputRecord.cs new file mode 100644 index 000000000..55d51bfed --- /dev/null +++ b/src/unit-tests/graphql-aspnet-tests/Internal/Templating/ObjectTypeTests/InputRecord.cs @@ -0,0 +1,21 @@ +// ************************************************************* +// project: graphql-aspnet +// -- +// repo: https://github.com/graphql-aspnet +// docs: https://graphql-aspnet.github.io +// -- +// License: MIT +// ************************************************************* + +namespace GraphQL.AspNet.Tests.Internal.Templating.ObjectTypeTests +{ + using GraphQL.AspNet.Attributes; + + [GraphType] + public record InputRecord + { + public int Property1 { get; set; } + + public string Property2 { get; set; } + } +} \ No newline at end of file diff --git a/src/unit-tests/graphql-aspnet-tests/Internal/Templating/ObjectTypeTests/ObjectRecord.cs b/src/unit-tests/graphql-aspnet-tests/Internal/Templating/ObjectTypeTests/ObjectRecord.cs new file mode 100644 index 000000000..b68ee914f --- /dev/null +++ b/src/unit-tests/graphql-aspnet-tests/Internal/Templating/ObjectTypeTests/ObjectRecord.cs @@ -0,0 +1,21 @@ +// ************************************************************* +// project: graphql-aspnet +// -- +// repo: https://github.com/graphql-aspnet +// docs: https://graphql-aspnet.github.io +// -- +// License: MIT +// ************************************************************* + +namespace GraphQL.AspNet.Tests.Internal.Templating.ObjectTypeTests +{ + using GraphQL.AspNet.Attributes; + + [GraphType] + public record ObjectRecord + { + public int Property1 { get; set; } + + public string Property2 { get; set; } + } +} \ No newline at end of file diff --git a/src/unit-tests/graphql-aspnet-tests/Internal/Templating/ObjectTypeTests/SimpleScalarStructWithTypeOverride.cs b/src/unit-tests/graphql-aspnet-tests/Internal/Templating/ObjectTypeTests/SimpleScalarStructWithTypeOverride.cs new file mode 100644 index 000000000..08507efa6 --- /dev/null +++ b/src/unit-tests/graphql-aspnet-tests/Internal/Templating/ObjectTypeTests/SimpleScalarStructWithTypeOverride.cs @@ -0,0 +1,19 @@ +// ************************************************************* +// project: graphql-aspnet +// -- +// repo: https://github.com/graphql-aspnet +// docs: https://graphql-aspnet.github.io +// -- +// License: MIT +// ************************************************************* + +namespace GraphQL.AspNet.Tests.Internal.Templating.ObjectTypeTests +{ + using GraphQL.AspNet.Attributes; + + [GraphType("SomeTypeName")] + public struct SimpleScalarStructWithTypeOverride + { + public int Prop1 { get; set; } + } +} \ No newline at end of file diff --git a/src/unit-tests/graphql-aspnet-tests/Internal/Templating/TypeExtensionFieldTemplateTests.cs b/src/unit-tests/graphql-aspnet-tests/Internal/Templating/TypeExtensionFieldTemplateTests.cs index af186ac77..078877919 100644 --- a/src/unit-tests/graphql-aspnet-tests/Internal/Templating/TypeExtensionFieldTemplateTests.cs +++ b/src/unit-tests/graphql-aspnet-tests/Internal/Templating/TypeExtensionFieldTemplateTests.cs @@ -201,6 +201,38 @@ public void ValidBatchExtension_WithCustomReturnType_AndNoDeclaredTypeOnAttribut Assert.AreEqual(FieldResolutionMode.Batch, template.Mode); } + [Test] + public void ValidBatchExtension_WithCustomNamedReturnType_PropertyCheck() + { + var methodInfo = typeof(ExtensionMethodController).GetMethod(nameof(ExtensionMethodController.Batch_CustomNamedObjectReturnedTestExtension)); + var template = this.CreateExtensionTemplate(nameof(ExtensionMethodController.Batch_CustomNamedObjectReturnedTestExtension)); + + Assert.AreEqual(methodInfo.ReflectedType, ((IGraphFieldResolverMethod)template).Parent.ObjectType); + Assert.AreEqual(typeof(TwoPropertyObject), template.SourceObjectType); + Assert.AreEqual(methodInfo, template.Method); + Assert.AreEqual("Custom_Named_Object", template.TypeExpression.ToString()); + Assert.AreEqual("[type]/TwoPropertyObject/fieldThree", template.Route.ToString()); + Assert.AreEqual(typeof(CustomNamedObject), template.ObjectType); + Assert.AreEqual(1, template.Arguments.Count); + Assert.AreEqual(FieldResolutionMode.Batch, template.Mode); + } + + [Test] + public void ValidBatchExtension_WithCustomNamedReturnType_OnSameCustomNamedParent_PropertyCheck() + { + var methodInfo = typeof(ExtensionMethodController).GetMethod(nameof(ExtensionMethodController.Batch_ChildIsSameCustomNamedObjectTestExtension)); + var template = this.CreateExtensionTemplate(nameof(ExtensionMethodController.Batch_ChildIsSameCustomNamedObjectTestExtension)); + + Assert.AreEqual(methodInfo.ReflectedType, ((IGraphFieldResolverMethod)template).Parent.ObjectType); + Assert.AreEqual(typeof(CustomNamedObject), template.SourceObjectType); + Assert.AreEqual(methodInfo, template.Method); + Assert.AreEqual("Custom_Named_Object", template.TypeExpression.ToString()); + Assert.AreEqual("[type]/Custom_Named_Object/fieldThree", template.Route.ToString()); + Assert.AreEqual(typeof(CustomNamedObject), template.ObjectType); + Assert.AreEqual(1, template.Arguments.Count); + Assert.AreEqual(FieldResolutionMode.Batch, template.Mode); + } + [Test] public void BatchExtension_NoSourceDataEnumerable_ThrowsException() { diff --git a/src/unit-tests/graphql-aspnet-tests/Schemas/GraphExecutionMessageTests.cs b/src/unit-tests/graphql-aspnet-tests/Schemas/GraphExecutionMessageTests.cs new file mode 100644 index 000000000..fd91e6a1d --- /dev/null +++ b/src/unit-tests/graphql-aspnet-tests/Schemas/GraphExecutionMessageTests.cs @@ -0,0 +1,88 @@ +// ************************************************************* +// project: graphql-aspnet +// -- +// repo: https://github.com/graphql-aspnet +// docs: https://graphql-aspnet.github.io +// -- +// License: MIT +// ************************************************************* + +namespace GraphQL.AspNet.Tests.Schemas +{ + using GraphQL.AspNet.Execution; + using NUnit.Framework; + + [TestFixture] + public class GraphExecutionMessageTests + { + [TestCase("BOB", "BOB", "JOE", "JOE")] + [TestCase("", "", "", "")] + [TestCase(" ", "", " ", "")] + [TestCase(null, Constants.ErrorCodes.DEFAULT, null, Constants.ErrorCodes.DEFAULT)] + [TestCase(null, Constants.ErrorCodes.DEFAULT, "custom code ", "custom code")] + public void CodeTests(string initialValue, string expectedValue, string updatedValue, string expectedUpdatedValue) + { + var message = new GraphExecutionMessage(GraphMessageSeverity.Critical, "message", code: initialValue); + Assert.AreEqual(expectedValue, message.Code); + + message.Code = updatedValue; + Assert.AreEqual(expectedUpdatedValue, message.Code); + } + + [TestCase("BOB", "BOB", "JOIE", "JOIE")] + [TestCase("", "", "", "")] + [TestCase(" ", "", " ", "")] + [TestCase(null, null, "jane ", "jane")] + public void MessageTests(string initialValue, string expectedValue, string updatedValue, string expectedUpdatedValue) + { + var message = new GraphExecutionMessage(GraphMessageSeverity.Critical, message: initialValue); + Assert.AreEqual(expectedValue, message.Message); + + message.Message = updatedValue; + Assert.AreEqual(expectedUpdatedValue, message.Message); + } + + [Test] + public void ExceptionTest() + { + var ex = new System.Exception("test"); + var message = new GraphExecutionMessage( + GraphMessageSeverity.Critical, + message: "message", + exception: ex); + + Assert.AreEqual(ex, message.Exception); + } + + [Test] + public void ExceptionTest_WithRemoval() + { + var ex = new System.Exception("test"); + var message = new GraphExecutionMessage( + GraphMessageSeverity.Critical, + message: "message", + exception: ex); + + Assert.AreEqual(ex, message.Exception); + + message.Exception = null; + Assert.IsNull(message.Exception); + } + + [Test] + public void ExceptionTest_WithReplacement() + { + var ex = new System.Exception("test"); + var message = new GraphExecutionMessage( + GraphMessageSeverity.Critical, + message: "message", + exception: ex); + + Assert.AreEqual(ex, message.Exception); + + var ex2 = new System.Exception("test again"); + message.Exception = ex2; + Assert.AreEqual(ex2, message.Exception); + } + } +} diff --git a/src/unit-tests/graphql-aspnet-tests/Schemas/GraphTypeExpressionTests.cs b/src/unit-tests/graphql-aspnet-tests/Schemas/GraphTypeExpressionTests.cs index 6b4df0498..320d667b2 100644 --- a/src/unit-tests/graphql-aspnet-tests/Schemas/GraphTypeExpressionTests.cs +++ b/src/unit-tests/graphql-aspnet-tests/Schemas/GraphTypeExpressionTests.cs @@ -197,13 +197,13 @@ public void AreCompatiable(string targetExpression, string suppliedExpression, b Assert.AreEqual(shouldBeCompatiable, result); } - [TestCase(typeof(int), "int!", null)] - [TestCase(typeof(IEnumerable), "[int!]", null)] - [TestCase(typeof(int[]), "[int!]", null)] + [TestCase(typeof(int), "Int!", null)] + [TestCase(typeof(IEnumerable), "[Int!]", null)] + [TestCase(typeof(int[]), "[Int!]", null)] [TestCase(typeof(IEnumerable), "[TwoPropertyObject]", null)] [TestCase(typeof(TwoPropertyObject[]), "[TwoPropertyObject]", null)] - [TestCase(typeof(int[][]), "[[int!]]", null)] - [TestCase(typeof(string[]), "[string]", null)] + [TestCase(typeof(int[][]), "[[Int!]]", null)] + [TestCase(typeof(string[]), "[String]", null)] [TestCase(typeof(KeyValuePair), "KeyValuePair_string_int_!", null)] [TestCase(typeof(KeyValuePair), "KeyValuePair_string___int_____!", null)] [TestCase(typeof(KeyValuePair), "KeyValuePair_string_int___!", null)] @@ -212,8 +212,8 @@ public void AreCompatiable(string targetExpression, string suppliedExpression, b [TestCase(typeof(KeyValuePair[][]), "[[KeyValuePair_string_____int_______!]]", null)] [TestCase(typeof(KeyValuePair[]), "[KeyValuePair_string_int_!]", null)] [TestCase(typeof(List>), "[KeyValuePair_string_int_!]", null)] - [TestCase(typeof(IEnumerable>), "[[int!]]", null)] - [TestCase(typeof(IEnumerable>), "int!", new MGT[] { MGT.IsNotNull })] + [TestCase(typeof(IEnumerable>), "[[Int!]]", null)] + [TestCase(typeof(IEnumerable>), "Int!", new MGT[] { MGT.IsNotNull })] public void GenerateTypeExpression( Type type, string expectedExpression, diff --git a/src/unit-tests/graphql-aspnet-tests/ServerExtensions/MutlipartRequests/ConfigTests.cs b/src/unit-tests/graphql-aspnet-tests/ServerExtensions/MutlipartRequests/ConfigTests.cs index 5d5224b1b..5b6a8f6a3 100644 --- a/src/unit-tests/graphql-aspnet-tests/ServerExtensions/MutlipartRequests/ConfigTests.cs +++ b/src/unit-tests/graphql-aspnet-tests/ServerExtensions/MutlipartRequests/ConfigTests.cs @@ -30,7 +30,7 @@ namespace GraphQL.AspNet.Tests.ServerExtensions.MutlipartRequests [TestFixture] public class ConfigTests { - private (MultiPartHttpFormPayloadParser, HttpContext) CreateTestObject( + private (MultiPartHttpFormPayloadParser Parser, HttpContext Context) CreateTestObject( string operationsField, string mapField, IMultipartRequestConfiguration config = null, @@ -75,7 +75,7 @@ public class ConfigTests formFile.Headers = new HeaderDictionary(); if (item.ContentType != null) - formFile.Headers.Add("Content-Type", item.ContentType); + formFile.Headers.Append("Content-Type", item.ContentType); fileCollection.Add(formFile); } diff --git a/src/unit-tests/graphql-aspnet-tests/ServerExtensions/MutlipartRequests/MultipartRequestGraphQLHttpProcessorTests.cs b/src/unit-tests/graphql-aspnet-tests/ServerExtensions/MutlipartRequests/MultipartRequestGraphQLHttpProcessorTests.cs index fd0746d14..ce0d1686a 100644 --- a/src/unit-tests/graphql-aspnet-tests/ServerExtensions/MutlipartRequests/MultipartRequestGraphQLHttpProcessorTests.cs +++ b/src/unit-tests/graphql-aspnet-tests/ServerExtensions/MutlipartRequests/MultipartRequestGraphQLHttpProcessorTests.cs @@ -22,7 +22,6 @@ namespace GraphQL.AspNet.Tests.ServerExtensions.MutlipartRequests using GraphQL.AspNet.Interfaces.Logging; using GraphQL.AspNet.Interfaces.Security; using GraphQL.AspNet.Schemas; - using GraphQL.AspNet.ServerExtensions.MultipartRequests; using GraphQL.AspNet.ServerExtensions.MultipartRequests.Configuration; using GraphQL.AspNet.ServerExtensions.MultipartRequests.Engine; using GraphQL.AspNet.ServerExtensions.MultipartRequests.Engine.TypeMakers; @@ -44,7 +43,7 @@ public class MultipartRequestGraphQLHttpProcessorTests { private DateTime _staticFailDate = new DateTime(202, 3, 4, 13, 14, 15, DateTimeKind.Utc); - private (HttpContext, MultipartRequestGraphQLHttpProcessor) CreateTestObjects( + private (HttpContext Context, MultipartRequestGraphQLHttpProcessor Processor) CreateTestObjects( (string FieldName, string FieldValue)[] fields = null, (string FieldName, string FileName, string ContentType, string FileContents)[] files = null, string httpMethod = "POST", @@ -80,7 +79,7 @@ public class MultipartRequestGraphQLHttpProcessorTests formFile.Headers = new HeaderDictionary(); if (item.ContentType != null) - formFile.Headers.Add("Content-Type", item.ContentType); + formFile.Headers.Append("Content-Type", item.ContentType); fileCollection.Add(formFile); } diff --git a/src/unit-tests/graphql-aspnet-tests/ServerExtensions/MutlipartRequests/MultipartRequestPayloadAssemblerTests.cs b/src/unit-tests/graphql-aspnet-tests/ServerExtensions/MutlipartRequests/MultipartRequestPayloadAssemblerTests.cs index 0c1cc2ef6..255af8534 100644 --- a/src/unit-tests/graphql-aspnet-tests/ServerExtensions/MutlipartRequests/MultipartRequestPayloadAssemblerTests.cs +++ b/src/unit-tests/graphql-aspnet-tests/ServerExtensions/MutlipartRequests/MultipartRequestPayloadAssemblerTests.cs @@ -32,7 +32,7 @@ namespace GraphQL.AspNet.Tests.ServerExtensions.MutlipartRequests [TestFixture] public class MultipartRequestPayloadAssemblerTests { - private (MultiPartHttpFormPayloadParser, HttpContext) CreateTestObject( + private (MultiPartHttpFormPayloadParser Parser, HttpContext Context) CreateTestObject( string operationsField, string mapField, IMultipartRequestConfiguration config = null, @@ -77,7 +77,7 @@ public class MultipartRequestPayloadAssemblerTests formFile.Headers = new HeaderDictionary(); if (item.ContentType != null) - formFile.Headers.Add("Content-Type", item.ContentType); + formFile.Headers.Append("Content-Type", item.ContentType); fileCollection.Add(formFile); } diff --git a/src/unit-tests/graphql-aspnet-tests/graphql-aspnet-tests.csproj b/src/unit-tests/graphql-aspnet-tests/graphql-aspnet-tests.csproj index a026d01d2..ef818ca14 100644 --- a/src/unit-tests/graphql-aspnet-tests/graphql-aspnet-tests.csproj +++ b/src/unit-tests/graphql-aspnet-tests/graphql-aspnet-tests.csproj @@ -1,31 +1,11 @@  + - net7.0;net6.0; - latest - $(NoWarn);1701;1702;1705;1591;NU1603;RCS1021;IDE0060;IDE0052;IDE0044;IDE0059;IDE0052;IDE0017;IDE0039;RCS1090;RCS1118;SA1601;RCS1163 GraphQL.AspNet.Tests graphql-aspnet-tests - true - true - false - - ..\..\styles.ruleset - - - - - - - - - - - - -