From afb8b534bb1a75f804ec90066484efa81f2a5f43 Mon Sep 17 00:00:00 2001 From: David Lebee Date: Wed, 3 Feb 2021 19:51:23 -0500 Subject: [PATCH] basic query and mutation discovery, next dynamic queries get added through extensions :) --- Demo/Demo.csproj | 2 + Demo/Startup.cs | 25 ++++-- .../Discovery/CommandMeta.cs | 14 ++++ .../Discovery/ICommandMeta.cs | 1 + .../Discovery/IQueryMeta.cs | 1 + .../Discovery/QueryMeta.cs | 14 ++++ .../Mvc/CommandControllerConvention.cs | 2 +- .../Mvc/QueryControllerConvention.cs | 2 +- .../Mvc/DynamicQueryControllerConvention.cs | 2 +- .../MutationObjectType.cs | 84 +++++++++++++++++++ ...weredSoft.CQRS.GraphQL.HotChocolate.csproj | 19 +++++ .../QueryObjectType.cs | 74 ++++++++++++++++ .../RequestExecutorBuilderExtensions.cs | 22 +++++ PoweredSoft.CQRS.sln | 6 ++ 14 files changed, 258 insertions(+), 10 deletions(-) create mode 100644 PoweredSoft.CQRS.GraphQL.HotChocolate/MutationObjectType.cs create mode 100644 PoweredSoft.CQRS.GraphQL.HotChocolate/PoweredSoft.CQRS.GraphQL.HotChocolate.csproj create mode 100644 PoweredSoft.CQRS.GraphQL.HotChocolate/QueryObjectType.cs create mode 100644 PoweredSoft.CQRS.GraphQL.HotChocolate/RequestExecutorBuilderExtensions.cs diff --git a/Demo/Demo.csproj b/Demo/Demo.csproj index b558cd1..7abba2b 100644 --- a/Demo/Demo.csproj +++ b/Demo/Demo.csproj @@ -6,6 +6,7 @@ + @@ -17,6 +18,7 @@ + diff --git a/Demo/Startup.cs b/Demo/Startup.cs index 988a41a..ed21cbe 100644 --- a/Demo/Startup.cs +++ b/Demo/Startup.cs @@ -4,6 +4,7 @@ using Demo.DynamicQueries; using Demo.Queries; using FluentValidation; using FluentValidation.AspNetCore; +using HotChocolate.Types; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.HttpsPolicy; @@ -18,6 +19,7 @@ using PoweredSoft.CQRS.AspNetCore.Mvc; using PoweredSoft.CQRS.DynamicQuery; using PoweredSoft.CQRS.DynamicQuery.Abstractions; using PoweredSoft.CQRS.DynamicQuery.AspNetCore; +using PoweredSoft.CQRS.GraphQL.HotChocolate; using PoweredSoft.Data; using PoweredSoft.Data.Core; using PoweredSoft.DynamicQuery; @@ -27,7 +29,7 @@ using System.Linq; using System.Threading.Tasks; namespace Demo -{ +{ public class Startup { public Startup(IConfiguration configuration) @@ -56,7 +58,15 @@ namespace Demo .AddPoweredSoftDynamicQueries() .AddFluentValidation(); - services.AddSwaggerGen(); + services + .AddGraphQLServer() + .AddQueryType(d => d.Name("Query")) + .AddPoweredSoftQueries() + .AddMutationType(d => d.Name("Mutation")) + .AddPoweredSoftMutations(); + + + //services.AddSwaggerGen(); } private void AddDynamicQueries(IServiceCollection services) @@ -100,18 +110,19 @@ namespace Demo app.UseAuthorization(); - app.UseSwagger(); + //app.UseSwagger(); // Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.), // specifying the Swagger JSON endpoint. - app.UseSwaggerUI(c => - { - c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); - }); + //app.UseSwaggerUI(c => + //{ + // c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); + //}); app.UseEndpoints(endpoints => { endpoints.MapControllers(); + endpoints.MapGraphQL(); }); } } diff --git a/PoweredSoft.CQRS.Abstractions/Discovery/CommandMeta.cs b/PoweredSoft.CQRS.Abstractions/Discovery/CommandMeta.cs index f875d43..1645151 100644 --- a/PoweredSoft.CQRS.Abstractions/Discovery/CommandMeta.cs +++ b/PoweredSoft.CQRS.Abstractions/Discovery/CommandMeta.cs @@ -33,5 +33,19 @@ namespace PoweredSoft.CQRS.Abstractions.Discovery public virtual Type CommandType { get; } public virtual Type ServiceType { get; } public virtual Type CommandResultType { get; } + + public string LowerCamelCaseName + { + get + { + if (string.IsNullOrEmpty(Name)) + return Name; + + var name = Name; + var firstLetter = Char.ToLowerInvariant(name[0]); + var ret = $"{firstLetter}{name.Substring(1)}"; + return ret; + } + } } } diff --git a/PoweredSoft.CQRS.Abstractions/Discovery/ICommandMeta.cs b/PoweredSoft.CQRS.Abstractions/Discovery/ICommandMeta.cs index 96720fc..cbfb257 100644 --- a/PoweredSoft.CQRS.Abstractions/Discovery/ICommandMeta.cs +++ b/PoweredSoft.CQRS.Abstractions/Discovery/ICommandMeta.cs @@ -8,5 +8,6 @@ namespace PoweredSoft.CQRS.Abstractions.Discovery Type CommandType { get; } Type ServiceType { get; } Type CommandResultType { get; } + string LowerCamelCaseName { get; } } } diff --git a/PoweredSoft.CQRS.Abstractions/Discovery/IQueryMeta.cs b/PoweredSoft.CQRS.Abstractions/Discovery/IQueryMeta.cs index f200d55..cb10539 100644 --- a/PoweredSoft.CQRS.Abstractions/Discovery/IQueryMeta.cs +++ b/PoweredSoft.CQRS.Abstractions/Discovery/IQueryMeta.cs @@ -12,5 +12,6 @@ namespace PoweredSoft.CQRS.Abstractions.Discovery Type ServiceType { get; } Type QueryResultType { get; } string Category { get; } + string LowerCamelCaseName { get; } } } diff --git a/PoweredSoft.CQRS.Abstractions/Discovery/QueryMeta.cs b/PoweredSoft.CQRS.Abstractions/Discovery/QueryMeta.cs index a8dd58f..d866adc 100644 --- a/PoweredSoft.CQRS.Abstractions/Discovery/QueryMeta.cs +++ b/PoweredSoft.CQRS.Abstractions/Discovery/QueryMeta.cs @@ -28,5 +28,19 @@ namespace PoweredSoft.CQRS.Abstractions.Discovery public virtual Type ServiceType { get; } public virtual Type QueryResultType { get; } public virtual string Category => "BasicQuery"; + + public string LowerCamelCaseName + { + get + { + if (string.IsNullOrEmpty(Name)) + return Name; + + var name = Name; + var firstLetter = Char.ToLowerInvariant(name[0]); + var ret = $"{firstLetter}{name.Substring(1)}"; + return ret; + } + } } } diff --git a/PoweredSoft.CQRS.AspNetCore/Mvc/CommandControllerConvention.cs b/PoweredSoft.CQRS.AspNetCore/Mvc/CommandControllerConvention.cs index 4962115..eb5c5e7 100644 --- a/PoweredSoft.CQRS.AspNetCore/Mvc/CommandControllerConvention.cs +++ b/PoweredSoft.CQRS.AspNetCore/Mvc/CommandControllerConvention.cs @@ -21,7 +21,7 @@ namespace PoweredSoft.CQRS.AspNetCore.Mvc var genericType = controller.ControllerType.GenericTypeArguments[0]; var commandDiscovery = this.serviceProvider.GetRequiredService(); var command = commandDiscovery.FindCommand(genericType); - controller.ControllerName = command.Name; + controller.ControllerName = command.LowerCamelCaseName; } } } diff --git a/PoweredSoft.CQRS.AspNetCore/Mvc/QueryControllerConvention.cs b/PoweredSoft.CQRS.AspNetCore/Mvc/QueryControllerConvention.cs index b3b740c..ccbf0a1 100644 --- a/PoweredSoft.CQRS.AspNetCore/Mvc/QueryControllerConvention.cs +++ b/PoweredSoft.CQRS.AspNetCore/Mvc/QueryControllerConvention.cs @@ -21,7 +21,7 @@ namespace PoweredSoft.CQRS.AspNetCore.Mvc var genericType = controller.ControllerType.GenericTypeArguments[0]; var queryDiscovery = this.serviceProvider.GetRequiredService(); var query = queryDiscovery.FindQuery(genericType); - controller.ControllerName = query.Name; + controller.ControllerName = query.LowerCamelCaseName; } } } diff --git a/PoweredSoft.CQRS.DynamicQuery.AspNetCore/Mvc/DynamicQueryControllerConvention.cs b/PoweredSoft.CQRS.DynamicQuery.AspNetCore/Mvc/DynamicQueryControllerConvention.cs index cd37b0a..2f90166 100644 --- a/PoweredSoft.CQRS.DynamicQuery.AspNetCore/Mvc/DynamicQueryControllerConvention.cs +++ b/PoweredSoft.CQRS.DynamicQuery.AspNetCore/Mvc/DynamicQueryControllerConvention.cs @@ -23,7 +23,7 @@ namespace PoweredSoft.CQRS.DynamicQuery.AspNetCore.Mvc var genericType = controller.ControllerType.GenericTypeArguments[0]; var queryDiscovery = this.serviceProvider.GetRequiredService(); var query = queryDiscovery.FindQuery(genericType); - controller.ControllerName = query.Name; + controller.ControllerName = query.LowerCamelCaseName; } } } diff --git a/PoweredSoft.CQRS.GraphQL.HotChocolate/MutationObjectType.cs b/PoweredSoft.CQRS.GraphQL.HotChocolate/MutationObjectType.cs new file mode 100644 index 0000000..9452676 --- /dev/null +++ b/PoweredSoft.CQRS.GraphQL.HotChocolate/MutationObjectType.cs @@ -0,0 +1,84 @@ +using HotChocolate.Resolvers; +using HotChocolate.Types; +using PoweredSoft.CQRS.Abstractions; +using PoweredSoft.CQRS.Abstractions.Discovery; +using System; +using System.Collections.Generic; + +namespace PoweredSoft.CQRS.GraphQL.HotChocolate +{ + public class MutationObjectType : ObjectTypeExtension + { + private readonly ICommandDiscovery commandDiscovery; + + public MutationObjectType(ICommandDiscovery commandDiscovery) : base() + { + this.commandDiscovery = commandDiscovery; + } + + protected override void Configure(IObjectTypeDescriptor desc) + { + desc.Name("Mutation"); + foreach (var m in commandDiscovery.GetCommands()) + { + var queryField = desc.Field(m.LowerCamelCaseName); + + Type typeToGet; + if (m.CommandResultType == null) + typeToGet = typeof(ICommandHandler<>).MakeGenericType(m.CommandType); + else + typeToGet = typeof(ICommandHandler<,>).MakeGenericType(m.CommandType, m.CommandResultType); + + if (m.CommandResultType == null) + queryField.Type(typeof(int?)); + else + queryField.Type(m.CommandResultType); + + //queryField.Use((sp, d) => new MutationAuthorizationMiddleware(m.CommandType, d)); + + if (m.CommandType.GetProperties().Length == 0) + { + queryField.Resolve(async ctx => + { + var queryArgument = Activator.CreateInstance(m.CommandType); + return await HandleMutation(m.CommandResultType != null, ctx, typeToGet, queryArgument); + }); + + continue; + } + + queryField.Argument("params", t => t.Type(m.CommandType)); + + queryField.Resolve(async ctx => + { + var queryArgument = ctx.ArgumentValue("params"); + return await HandleMutation(m.CommandResultType != null, ctx, typeToGet, queryArgument); + }); + + // TODO. + //if (m.MutationObjectRequired) + // queryField.Use(); + + // TODO. + //if (m.ValidateMutationObject) + // queryField.Use(); + } + } + + private async System.Threading.Tasks.Task HandleMutation(bool hasResult, IResolverContext ctx, Type typeToGet, object queryArgument) + { + dynamic service = ctx.Service(typeToGet); + + if (hasResult) + { + var result = await service.HandleAsync((dynamic)queryArgument); + return result; + } + else + { + await service.HandleAsync((dynamic)queryArgument); + return null; + } + } + } +} diff --git a/PoweredSoft.CQRS.GraphQL.HotChocolate/PoweredSoft.CQRS.GraphQL.HotChocolate.csproj b/PoweredSoft.CQRS.GraphQL.HotChocolate/PoweredSoft.CQRS.GraphQL.HotChocolate.csproj new file mode 100644 index 0000000..245df3f --- /dev/null +++ b/PoweredSoft.CQRS.GraphQL.HotChocolate/PoweredSoft.CQRS.GraphQL.HotChocolate.csproj @@ -0,0 +1,19 @@ + + + + netcoreapp3.1 + + + + + + + + + + + + + + + diff --git a/PoweredSoft.CQRS.GraphQL.HotChocolate/QueryObjectType.cs b/PoweredSoft.CQRS.GraphQL.HotChocolate/QueryObjectType.cs new file mode 100644 index 0000000..24c3f5e --- /dev/null +++ b/PoweredSoft.CQRS.GraphQL.HotChocolate/QueryObjectType.cs @@ -0,0 +1,74 @@ +using HotChocolate.Language; +using HotChocolate.Resolvers; +using HotChocolate.Types; +using PoweredSoft.CQRS.Abstractions; +using PoweredSoft.CQRS.Abstractions.Discovery; +using System; +using System.Text; + +namespace PoweredSoft.CQRS.GraphQL.HotChocolate +{ + public class QueryObjectType : ObjectTypeExtension + { + private readonly IQueryDiscovery queryDiscovery; + + public QueryObjectType(IQueryDiscovery queryDiscovery) : base() + { + this.queryDiscovery = queryDiscovery; + } + + protected override void Configure(IObjectTypeDescriptor desc) + { + desc.Name("Query"); + foreach (var q in queryDiscovery.GetQueries()) + { + if (q.Category != "BasicQuery") + return; + + var queryField = desc.Field(q.LowerCamelCaseName); + var typeToGet = typeof(IQueryHandler<,>).MakeGenericType(q.QueryType, q.QueryResultType); + + queryField.Type(q.QueryResultType); + + // TODO. + // always required. + //queryField.Use((sp, d) => new QueryAuthorizationMiddleware(q.QueryType, d)); + + if (q.QueryType.GetProperties().Length == 0) + { + queryField.Resolve(async ctx => + { + var queryArgument = Activator.CreateInstance(q.QueryType); + return await HandleQuery(ctx, typeToGet, queryArgument); + }); + + continue; + } + + queryField.Argument("params", t => t.Type(q.QueryType)); + + queryField.Resolve(async ctx => + { + var queryArgument = ctx.ArgumentValue("params"); + return await HandleQuery(ctx, typeToGet, queryArgument); + }); + + /* + if (q.QueryObjectRequired) + queryField.Use();*/ + + /* TODO + if (q.ValidateQueryObject) + queryField.Use(); + */ + } + } + + private async System.Threading.Tasks.Task HandleQuery(IResolverContext resolverContext, Type typeToGet, object queryArgument) + { + dynamic service = resolverContext.Service(typeToGet); + var result = await service.HandleAsync((dynamic)queryArgument); + return result; + } + } +} \ No newline at end of file diff --git a/PoweredSoft.CQRS.GraphQL.HotChocolate/RequestExecutorBuilderExtensions.cs b/PoweredSoft.CQRS.GraphQL.HotChocolate/RequestExecutorBuilderExtensions.cs new file mode 100644 index 0000000..05f7804 --- /dev/null +++ b/PoweredSoft.CQRS.GraphQL.HotChocolate/RequestExecutorBuilderExtensions.cs @@ -0,0 +1,22 @@ +using HotChocolate; +using HotChocolate.Execution.Configuration; +using Microsoft.Extensions.DependencyInjection; +using System; + +namespace PoweredSoft.CQRS.GraphQL.HotChocolate +{ + public static class RequestExecutorBuilderExtensions + { + public static IRequestExecutorBuilder AddPoweredSoftQueries(this IRequestExecutorBuilder builder) + { + builder.AddTypeExtension(); + return builder; + } + + public static IRequestExecutorBuilder AddPoweredSoftMutations(this IRequestExecutorBuilder builder) + { + builder.AddTypeExtension(); + return builder; + } + } +} diff --git a/PoweredSoft.CQRS.sln b/PoweredSoft.CQRS.sln index 221bb14..f29c46d 100644 --- a/PoweredSoft.CQRS.sln +++ b/PoweredSoft.CQRS.sln @@ -25,6 +25,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PoweredSoft.CQRS.DynamicQue EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PoweredSoft.CQRS.DynamicQuery.AspNetCore", "PoweredSoft.CQRS.DynamicQuery.AspNetCore\PoweredSoft.CQRS.DynamicQuery.AspNetCore.csproj", "{0829B99A-0A20-4CAC-A91E-FB67E18444DE}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PoweredSoft.CQRS.GraphQL.HotChocolate", "PoweredSoft.CQRS.GraphQL.HotChocolate\PoweredSoft.CQRS.GraphQL.HotChocolate.csproj", "{BF8E3B0D-8651-4541-892F-F607C5E80F9B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -63,6 +65,10 @@ Global {0829B99A-0A20-4CAC-A91E-FB67E18444DE}.Debug|Any CPU.Build.0 = Debug|Any CPU {0829B99A-0A20-4CAC-A91E-FB67E18444DE}.Release|Any CPU.ActiveCfg = Release|Any CPU {0829B99A-0A20-4CAC-A91E-FB67E18444DE}.Release|Any CPU.Build.0 = Release|Any CPU + {BF8E3B0D-8651-4541-892F-F607C5E80F9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BF8E3B0D-8651-4541-892F-F607C5E80F9B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BF8E3B0D-8651-4541-892F-F607C5E80F9B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BF8E3B0D-8651-4541-892F-F607C5E80F9B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE