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