From 60abf83932722a61ffbcea8bf10857664984bb1f Mon Sep 17 00:00:00 2001 From: David Lebee Date: Tue, 2 Feb 2021 01:05:48 -0500 Subject: [PATCH] aspnetcore query support. --- Demo/Demo.csproj | 19 +++++ Demo/Program.cs | 26 +++++++ Demo/Properties/launchSettings.json | 28 +++++++ Demo/Queries/PersonQuery.cs | 50 ++++++++++++ Demo/Startup.cs | 76 +++++++++++++++++++ Demo/appsettings.Development.json | 9 +++ Demo/appsettings.json | 10 +++ .../CommandControllerIgnoreAttribute.cs | 9 +++ .../QueryControllerIgnoreAttribute.cs | 11 +++ ...edSoft.CQRS.AspNetCore.Abstractions.csproj | 7 ++ .../Mvc/QueryController.cs | 21 +++++ .../Mvc/QueryControllerConvention.cs | 28 +++++++ .../Mvc/QueryControllerFeatureProvider.cs | 32 ++++++++ .../Mvc/QueryControllerOptions.cs | 11 +++ .../MvcBuilderExensions.cs | 30 ++++++++ .../PoweredSoft.CQRS.AspNetCore.csproj | 15 ++++ PoweredSoft.CQRS.sln | 22 +++++- 17 files changed, 402 insertions(+), 2 deletions(-) create mode 100644 Demo/Demo.csproj create mode 100644 Demo/Program.cs create mode 100644 Demo/Properties/launchSettings.json create mode 100644 Demo/Queries/PersonQuery.cs create mode 100644 Demo/Startup.cs create mode 100644 Demo/appsettings.Development.json create mode 100644 Demo/appsettings.json create mode 100644 PoweredSoft.CQRS.AspNetCore.Abstractions/Attributes/CommandControllerIgnoreAttribute.cs create mode 100644 PoweredSoft.CQRS.AspNetCore.Abstractions/Attributes/QueryControllerIgnoreAttribute.cs create mode 100644 PoweredSoft.CQRS.AspNetCore.Abstractions/PoweredSoft.CQRS.AspNetCore.Abstractions.csproj create mode 100644 PoweredSoft.CQRS.AspNetCore/Mvc/QueryController.cs create mode 100644 PoweredSoft.CQRS.AspNetCore/Mvc/QueryControllerConvention.cs create mode 100644 PoweredSoft.CQRS.AspNetCore/Mvc/QueryControllerFeatureProvider.cs create mode 100644 PoweredSoft.CQRS.AspNetCore/Mvc/QueryControllerOptions.cs create mode 100644 PoweredSoft.CQRS.AspNetCore/MvcBuilderExensions.cs create mode 100644 PoweredSoft.CQRS.AspNetCore/PoweredSoft.CQRS.AspNetCore.csproj diff --git a/Demo/Demo.csproj b/Demo/Demo.csproj new file mode 100644 index 0000000..73ebaee --- /dev/null +++ b/Demo/Demo.csproj @@ -0,0 +1,19 @@ + + + + netcoreapp3.1 + + + + + + + + + + + + + + + diff --git a/Demo/Program.cs b/Demo/Program.cs new file mode 100644 index 0000000..7e5335f --- /dev/null +++ b/Demo/Program.cs @@ -0,0 +1,26 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Demo +{ + public 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(); + }); + } +} diff --git a/Demo/Properties/launchSettings.json b/Demo/Properties/launchSettings.json new file mode 100644 index 0000000..948de0d --- /dev/null +++ b/Demo/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:52483", + "sslPort": 44343 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": false, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Demo": { + "commandName": "Project", + "launchBrowser": false, + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Demo/Queries/PersonQuery.cs b/Demo/Queries/PersonQuery.cs new file mode 100644 index 0000000..bdbb1f3 --- /dev/null +++ b/Demo/Queries/PersonQuery.cs @@ -0,0 +1,50 @@ +using PoweredSoft.CQRS.Abstractions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Demo.Queries +{ + public class Person + { + public long Id { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + } + + public class PersonQuery + { + public string Search { get; set; } + } + + public class PersonQueryHandler : IQueryHandler> + { + private readonly IEnumerable _persons = new List() + { + new Person + { + Id = 1, + FirstName = "David", + LastName = "Lebee" + }, + new Person + { + Id = 2, + FirstName = "John", + LastName = "Doe" + } + }; + + public Task> HandleAsync(PersonQuery query, CancellationToken cancellationToken = default) + { + var ret = _persons.AsQueryable(); + + if (query != null && !string.IsNullOrEmpty(query.Search)) + ret = ret.Where(t => t.FirstName.Contains(query.Search) || t.LastName.Contains(query.Search)); + + return Task.FromResult(ret); + } + } +} diff --git a/Demo/Startup.cs b/Demo/Startup.cs new file mode 100644 index 0000000..101dbc3 --- /dev/null +++ b/Demo/Startup.cs @@ -0,0 +1,76 @@ +using Demo.Queries; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.HttpsPolicy; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using PoweredSoft.CQRS; +using PoweredSoft.CQRS.Abstractions; +using PoweredSoft.CQRS.AspNetCore.Mvc; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Demo +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + AddQueries(services); + + services.AddPoweredSoftCQRS(); + services + .AddControllers() + .AddPoweredSoftQueryController(); + + services.AddSwaggerGen(); + } + + private void AddQueries(IServiceCollection services) + { + services.AddQuery, PersonQueryHandler>(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseHttpsRedirection(); + + app.UseRouting(); + + app.UseAuthorization(); + + 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.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + } + } +} diff --git a/Demo/appsettings.Development.json b/Demo/appsettings.Development.json new file mode 100644 index 0000000..8983e0f --- /dev/null +++ b/Demo/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/Demo/appsettings.json b/Demo/appsettings.json new file mode 100644 index 0000000..d9d9a9b --- /dev/null +++ b/Demo/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/PoweredSoft.CQRS.AspNetCore.Abstractions/Attributes/CommandControllerIgnoreAttribute.cs b/PoweredSoft.CQRS.AspNetCore.Abstractions/Attributes/CommandControllerIgnoreAttribute.cs new file mode 100644 index 0000000..ef49927 --- /dev/null +++ b/PoweredSoft.CQRS.AspNetCore.Abstractions/Attributes/CommandControllerIgnoreAttribute.cs @@ -0,0 +1,9 @@ +using System; + +namespace PoweredSoft.CQRS.AspNetCore.Abstractions.Attributes +{ + [AttributeUsage(AttributeTargets.Class)] + public class CommandControllerIgnoreAttribute : Attribute + { + } +} diff --git a/PoweredSoft.CQRS.AspNetCore.Abstractions/Attributes/QueryControllerIgnoreAttribute.cs b/PoweredSoft.CQRS.AspNetCore.Abstractions/Attributes/QueryControllerIgnoreAttribute.cs new file mode 100644 index 0000000..7383cd0 --- /dev/null +++ b/PoweredSoft.CQRS.AspNetCore.Abstractions/Attributes/QueryControllerIgnoreAttribute.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace PoweredSoft.CQRS.AspNetCore.Abstractions.Attributes +{ + [AttributeUsage(AttributeTargets.Class)] + public class QueryControllerIgnoreAttribute : Attribute + { + } +} diff --git a/PoweredSoft.CQRS.AspNetCore.Abstractions/PoweredSoft.CQRS.AspNetCore.Abstractions.csproj b/PoweredSoft.CQRS.AspNetCore.Abstractions/PoweredSoft.CQRS.AspNetCore.Abstractions.csproj new file mode 100644 index 0000000..9f5c4f4 --- /dev/null +++ b/PoweredSoft.CQRS.AspNetCore.Abstractions/PoweredSoft.CQRS.AspNetCore.Abstractions.csproj @@ -0,0 +1,7 @@ + + + + netstandard2.0 + + + diff --git a/PoweredSoft.CQRS.AspNetCore/Mvc/QueryController.cs b/PoweredSoft.CQRS.AspNetCore/Mvc/QueryController.cs new file mode 100644 index 0000000..5526283 --- /dev/null +++ b/PoweredSoft.CQRS.AspNetCore/Mvc/QueryController.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Mvc; +using PoweredSoft.CQRS.Abstractions; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace PoweredSoft.CQRS.AspNetCore.Mvc +{ + [ApiController, Route("api/query/[controller]")] + public class QueryController : Controller + where TQuery : class + { + [HttpPost] + public Task Handle([FromServices] IQueryHandler handler, + [FromBody] TQuery query) + { + return handler.HandleAsync(query, this.Request.HttpContext.RequestAborted); + } + } +} diff --git a/PoweredSoft.CQRS.AspNetCore/Mvc/QueryControllerConvention.cs b/PoweredSoft.CQRS.AspNetCore/Mvc/QueryControllerConvention.cs new file mode 100644 index 0000000..b3b740c --- /dev/null +++ b/PoweredSoft.CQRS.AspNetCore/Mvc/QueryControllerConvention.cs @@ -0,0 +1,28 @@ +using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.Extensions.DependencyInjection; +using PoweredSoft.CQRS.Abstractions.Discovery; +using System; + +namespace PoweredSoft.CQRS.AspNetCore.Mvc +{ + public class QueryControllerConvention : IControllerModelConvention + { + private readonly IServiceProvider serviceProvider; + + public QueryControllerConvention(IServiceProvider serviceProvider) + { + this.serviceProvider = serviceProvider; + } + + public void Apply(ControllerModel controller) + { + if (controller.ControllerType.IsGenericType && controller.ControllerType.Name.Contains("QueryController") && controller.ControllerType.Assembly == typeof(QueryControllerConvention).Assembly) + { + var genericType = controller.ControllerType.GenericTypeArguments[0]; + var queryDiscovery = this.serviceProvider.GetRequiredService(); + var query = queryDiscovery.FindQuery(genericType); + controller.ControllerName = query.Name; + } + } + } +} diff --git a/PoweredSoft.CQRS.AspNetCore/Mvc/QueryControllerFeatureProvider.cs b/PoweredSoft.CQRS.AspNetCore/Mvc/QueryControllerFeatureProvider.cs new file mode 100644 index 0000000..02d0ad3 --- /dev/null +++ b/PoweredSoft.CQRS.AspNetCore/Mvc/QueryControllerFeatureProvider.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Mvc.ApplicationParts; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.Extensions.DependencyInjection; +using PoweredSoft.CQRS.Abstractions.Discovery; +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text; + +namespace PoweredSoft.CQRS.AspNetCore.Mvc +{ + public class QueryControllerFeatureProvider : IApplicationFeatureProvider + { + private readonly ServiceProvider serviceProvider; + + public QueryControllerFeatureProvider(ServiceProvider serviceProvider) + { + this.serviceProvider = serviceProvider; + } + + public void PopulateFeature(IEnumerable parts, ControllerFeature feature) + { + var queryDiscovery = this.serviceProvider.GetRequiredService(); + foreach (var f in queryDiscovery.GetQueries()) + { + var controllerType = typeof(QueryController<,>).MakeGenericType(f.QueryType, f.QueryResultType); + var controllerTypeInfo = controllerType.GetTypeInfo(); + feature.Controllers.Add(controllerTypeInfo); + } + } + } +} diff --git a/PoweredSoft.CQRS.AspNetCore/Mvc/QueryControllerOptions.cs b/PoweredSoft.CQRS.AspNetCore/Mvc/QueryControllerOptions.cs new file mode 100644 index 0000000..0474580 --- /dev/null +++ b/PoweredSoft.CQRS.AspNetCore/Mvc/QueryControllerOptions.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace PoweredSoft.CQRS.AspNetCore.Mvc +{ + public class QueryControllerOptions + { + + } +} diff --git a/PoweredSoft.CQRS.AspNetCore/MvcBuilderExensions.cs b/PoweredSoft.CQRS.AspNetCore/MvcBuilderExensions.cs new file mode 100644 index 0000000..5c8ea49 --- /dev/null +++ b/PoweredSoft.CQRS.AspNetCore/MvcBuilderExensions.cs @@ -0,0 +1,30 @@ +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Text; + +namespace PoweredSoft.CQRS.AspNetCore.Mvc +{ + public static class MvcBuilderExtensions + { + public static IMvcBuilder AddPoweredSoftQueryController(this IMvcBuilder builder, Action configuration = null) + { + var options = new QueryControllerOptions(); + configuration?.Invoke(options); + var services = builder.Services; + var serviceProvider = services.BuildServiceProvider(); + builder.AddMvcOptions(o => o.Conventions.Add(new QueryControllerConvention(serviceProvider))); + builder.ConfigureApplicationPartManager(m => m.FeatureProviders.Add(new QueryControllerFeatureProvider(serviceProvider))); + return builder; + } + + /* + public static IMvcBuilder AddPoweredSoftCommandController(this IMvcBuilder builder) + { + var services = builder.Services; + var serviceProvider = services.BuildServiceProvider(); + builder.AddMvcOptions(o => o.Conventions.Add(new CommandControllerConvention(serviceProvider))); + builder.ConfigureApplicationPartManager(m => m.FeatureProviders.Add(new CommandControllerFeatureProvider(serviceProvider))); + return builder; + }*/ + } +} diff --git a/PoweredSoft.CQRS.AspNetCore/PoweredSoft.CQRS.AspNetCore.csproj b/PoweredSoft.CQRS.AspNetCore/PoweredSoft.CQRS.AspNetCore.csproj new file mode 100644 index 0000000..138f576 --- /dev/null +++ b/PoweredSoft.CQRS.AspNetCore/PoweredSoft.CQRS.AspNetCore.csproj @@ -0,0 +1,15 @@ + + + + netcoreapp3.1 + + + + + + + + + + + diff --git a/PoweredSoft.CQRS.sln b/PoweredSoft.CQRS.sln index f1d1358..379fbe1 100644 --- a/PoweredSoft.CQRS.sln +++ b/PoweredSoft.CQRS.sln @@ -3,9 +3,15 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.30907.101 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PoweredSoft.CQRS.Abstractions", "PoweredSoft.CQRS.Abstractions\PoweredSoft.CQRS.Abstractions.csproj", "{ED78E19D-31D4-4783-AE9E-2844A8541277}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PoweredSoft.CQRS.Abstractions", "PoweredSoft.CQRS.Abstractions\PoweredSoft.CQRS.Abstractions.csproj", "{ED78E19D-31D4-4783-AE9E-2844A8541277}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PoweredSoft.CQRS", "PoweredSoft.CQRS\PoweredSoft.CQRS.csproj", "{7069B98F-8736-4114-8AF5-1ACE094E6238}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PoweredSoft.CQRS", "PoweredSoft.CQRS\PoweredSoft.CQRS.csproj", "{7069B98F-8736-4114-8AF5-1ACE094E6238}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PoweredSoft.CQRS.AspNetCore", "PoweredSoft.CQRS.AspNetCore\PoweredSoft.CQRS.AspNetCore.csproj", "{A1D577E5-61BD-4E25-B2C8-1005C1D7665B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PoweredSoft.CQRS.AspNetCore.Abstractions", "PoweredSoft.CQRS.AspNetCore.Abstractions\PoweredSoft.CQRS.AspNetCore.Abstractions.csproj", "{4C466827-31D3-4081-A751-C2FC7C381D7E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Demo", "Demo\Demo.csproj", "{F15B1E11-8D4C-489E-AFF7-AA144105FE46}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -21,6 +27,18 @@ Global {7069B98F-8736-4114-8AF5-1ACE094E6238}.Debug|Any CPU.Build.0 = Debug|Any CPU {7069B98F-8736-4114-8AF5-1ACE094E6238}.Release|Any CPU.ActiveCfg = Release|Any CPU {7069B98F-8736-4114-8AF5-1ACE094E6238}.Release|Any CPU.Build.0 = Release|Any CPU + {A1D577E5-61BD-4E25-B2C8-1005C1D7665B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1D577E5-61BD-4E25-B2C8-1005C1D7665B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1D577E5-61BD-4E25-B2C8-1005C1D7665B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1D577E5-61BD-4E25-B2C8-1005C1D7665B}.Release|Any CPU.Build.0 = Release|Any CPU + {4C466827-31D3-4081-A751-C2FC7C381D7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4C466827-31D3-4081-A751-C2FC7C381D7E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4C466827-31D3-4081-A751-C2FC7C381D7E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4C466827-31D3-4081-A751-C2FC7C381D7E}.Release|Any CPU.Build.0 = Release|Any CPU + {F15B1E11-8D4C-489E-AFF7-AA144105FE46}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F15B1E11-8D4C-489E-AFF7-AA144105FE46}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F15B1E11-8D4C-489E-AFF7-AA144105FE46}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F15B1E11-8D4C-489E-AFF7-AA144105FE46}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE