diff --git a/Demo/Demo.csproj b/Demo/Demo.csproj
index dadea88..d1235b0 100644
--- a/Demo/Demo.csproj
+++ b/Demo/Demo.csproj
@@ -7,6 +7,7 @@
+
@@ -14,6 +15,7 @@
+
diff --git a/Demo/Startup.cs b/Demo/Startup.cs
index 61ee19d..4402ac8 100644
--- a/Demo/Startup.cs
+++ b/Demo/Startup.cs
@@ -24,6 +24,7 @@ using System.Linq;
using PoweredSoft.CQRS.GraphQL.HotChocolate.DynamicQuery;
using PoweredSoft.CQRS.Abstractions.Security;
using Demo.Security;
+using Microsoft.AspNet.OData.Extensions;
namespace Demo
{
@@ -50,14 +51,15 @@ namespace Demo
services.AddPoweredSoftDataServices();
services.AddPoweredSoftDynamicQuery();
- services
- .AddPoweredSoftCQRS();
+ services.AddPoweredSoftCQRS();
+ services.AddOData();
services
.AddControllers()
.AddPoweredSoftQueries()
.AddPoweredSoftCommands()
.AddPoweredSoftDynamicQueries()
+ .AddPoweredSoftODataQueries()
.AddFluentValidation();
services
@@ -128,6 +130,10 @@ namespace Demo
{
endpoints.MapControllers();
endpoints.MapGraphQL();
+
+ endpoints.Select().Filter().OrderBy().Count().MaxTop(10);
+
+ endpoints.MapODataRoute("odata", "odata", endpoints.GetPoweredSoftODataEdmModel());
});
}
}
diff --git a/PoweredSoft.CQRS.AspNetCore.OData.Abstractions/PoweredSoft.CQRS.AspNetCore.OData.Abstractions.csproj b/PoweredSoft.CQRS.AspNetCore.OData.Abstractions/PoweredSoft.CQRS.AspNetCore.OData.Abstractions.csproj
new file mode 100644
index 0000000..9f5c4f4
--- /dev/null
+++ b/PoweredSoft.CQRS.AspNetCore.OData.Abstractions/PoweredSoft.CQRS.AspNetCore.OData.Abstractions.csproj
@@ -0,0 +1,7 @@
+
+
+
+ netstandard2.0
+
+
+
diff --git a/PoweredSoft.CQRS.AspNetCore.OData.Abstractions/QueryControllerIgnoreAttribute.cs b/PoweredSoft.CQRS.AspNetCore.OData.Abstractions/QueryControllerIgnoreAttribute.cs
new file mode 100644
index 0000000..af85fe7
--- /dev/null
+++ b/PoweredSoft.CQRS.AspNetCore.OData.Abstractions/QueryControllerIgnoreAttribute.cs
@@ -0,0 +1,11 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace PoweredSoft.CQRS.AspNetCore.OData.Abstractions
+{
+ [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
+ public class QueryOdataControllerIgnoreAttribute : Attribute
+ {
+ }
+}
diff --git a/PoweredSoft.CQRS.AspNetCore.OData/EndpointExstensions.cs b/PoweredSoft.CQRS.AspNetCore.OData/EndpointExstensions.cs
new file mode 100644
index 0000000..abfe9cc
--- /dev/null
+++ b/PoweredSoft.CQRS.AspNetCore.OData/EndpointExstensions.cs
@@ -0,0 +1,44 @@
+using Microsoft.AspNet.OData.Builder;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.OData.Edm;
+using PoweredSoft.CQRS.Abstractions.Discovery;
+using PoweredSoft.CQRS.AspNetCore.OData.Abstractions;
+using System.Linq;
+using System.Reflection;
+
+namespace PoweredSoft.CQRS.AspNetCore.Mvc
+{
+ public static class EndpointExstensions
+ {
+ public static IEdmModel GetPoweredSoftODataEdmModel(this IEndpointRouteBuilder endpoint)
+ {
+ var queryDiscovery = endpoint.ServiceProvider.GetRequiredService();
+
+ var odataBuilder = new ODataConventionModelBuilder();
+ odataBuilder.EnableLowerCamelCase();
+
+ foreach(var q in queryDiscovery.GetQueries())
+ {
+ var ignoreAttribute = q.QueryType.GetCustomAttribute();
+ if (ignoreAttribute != null)
+ continue;
+
+ if (q.Category != "BasicQuery")
+ continue;
+
+ var isQueryable = q.QueryResultType.Namespace == "System.Linq" && q.QueryResultType.Name.Contains("IQueryable");
+ if (!isQueryable)
+ continue;
+
+
+ var entityType = q.QueryResultType.GetGenericArguments().First();
+ odataBuilder.GetType().GetMethod("EntitySet").MakeGenericMethod(entityType).Invoke(odataBuilder, new object[] {
+ q.LowerCamelCaseName
+ });
+ }
+
+ return odataBuilder.GetEdmModel();
+ }
+ }
+}
diff --git a/PoweredSoft.CQRS.AspNetCore.OData/MvcBuilderExensions.cs b/PoweredSoft.CQRS.AspNetCore.OData/MvcBuilderExensions.cs
new file mode 100644
index 0000000..5001341
--- /dev/null
+++ b/PoweredSoft.CQRS.AspNetCore.OData/MvcBuilderExensions.cs
@@ -0,0 +1,46 @@
+using Microsoft.AspNetCore.Mvc.Formatters;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Net.Http.Headers;
+using PoweredSoft.CQRS.AspNetCore.OData;
+using System;
+using System.Linq;
+using System.Text;
+
+namespace PoweredSoft.CQRS.AspNetCore.Mvc
+{
+
+ public static class MvcBuilderExtensions
+ {
+ public static IMvcBuilder AddPoweredSoftODataQueries(this IMvcBuilder builder, Action configuration = null)
+ {
+ var options = new QueryODataControllerOptions();
+ configuration?.Invoke(options);
+ var services = builder.Services;
+ var serviceProvider = services.BuildServiceProvider();
+ builder.AddMvcOptions(o => o.Conventions.Add(new QueryODataControllerConvention(serviceProvider)));
+ builder.ConfigureApplicationPartManager(m => m.FeatureProviders.Add(new QueryODataControllerFeatureProvider(serviceProvider)));
+
+ if (options.FixODataSwagger)
+ builder.FixODataSwagger();
+ return builder;
+ }
+
+ public static IMvcBuilder FixODataSwagger(this IMvcBuilder builder)
+ {
+ builder.AddMvcOptions(options =>
+ {
+ foreach (var outputFormatter in options.OutputFormatters.OfType().Where(x => x.SupportedMediaTypes.Count == 0))
+ {
+ outputFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/prs.odatatestxx-odata"));
+ }
+
+ foreach (var inputFormatter in options.InputFormatters.OfType().Where(x => x.SupportedMediaTypes.Count == 0))
+ {
+ inputFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/prs.odatatestxx-odata"));
+ }
+ });
+
+ return builder;
+ }
+ }
+}
diff --git a/PoweredSoft.CQRS.AspNetCore.OData/PoweredSoft.CQRS.AspNetCore.OData.csproj b/PoweredSoft.CQRS.AspNetCore.OData/PoweredSoft.CQRS.AspNetCore.OData.csproj
new file mode 100644
index 0000000..9ac5aae
--- /dev/null
+++ b/PoweredSoft.CQRS.AspNetCore.OData/PoweredSoft.CQRS.AspNetCore.OData.csproj
@@ -0,0 +1,21 @@
+
+
+
+ netcoreapp3.1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/PoweredSoft.CQRS.AspNetCore.OData/QueryODataController.cs b/PoweredSoft.CQRS.AspNetCore.OData/QueryODataController.cs
new file mode 100644
index 0000000..721c1d1
--- /dev/null
+++ b/PoweredSoft.CQRS.AspNetCore.OData/QueryODataController.cs
@@ -0,0 +1,23 @@
+using Microsoft.AspNet.OData;
+using Microsoft.AspNetCore.Mvc;
+using PoweredSoft.CQRS.Abstractions;
+using PoweredSoft.CQRS.AspNetCore.Mvc;
+using System;
+using System.Threading.Tasks;
+
+namespace PoweredSoft.CQRS.AspNetCore.OData
+{
+ [Route("api/odata/[controller]")]
+ [ApiExplorerSettings(IgnoreApi = true)]
+ public class QueryODataController : ODataController
+ where TQuery : class
+ {
+ [EnableQuery, HttpGet, QueryControllerAuthorization]
+ public async Task Get([FromServices]IQueryHandler queryHandler)
+ {
+ var result = await queryHandler.HandleAsync(null, HttpContext.RequestAborted);
+ return result;
+ }
+ }
+
+}
diff --git a/PoweredSoft.CQRS.AspNetCore.OData/QueryODataControllerConvention.cs b/PoweredSoft.CQRS.AspNetCore.OData/QueryODataControllerConvention.cs
new file mode 100644
index 0000000..e8cafcb
--- /dev/null
+++ b/PoweredSoft.CQRS.AspNetCore.OData/QueryODataControllerConvention.cs
@@ -0,0 +1,29 @@
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.ApplicationModels;
+using Microsoft.Extensions.DependencyInjection;
+using PoweredSoft.CQRS.Abstractions.Discovery;
+using System;
+
+namespace PoweredSoft.CQRS.AspNetCore.Mvc
+{
+ public class QueryODataControllerConvention : IControllerModelConvention
+ {
+ private readonly IServiceProvider serviceProvider;
+
+ public QueryODataControllerConvention(IServiceProvider serviceProvider)
+ {
+ this.serviceProvider = serviceProvider;
+ }
+
+ public void Apply(ControllerModel controller)
+ {
+ if (controller.ControllerType.IsGenericType && controller.ControllerType.Name.Contains("QueryODataController") && controller.ControllerType.Assembly == typeof(QueryODataControllerConvention).Assembly)
+ {
+ var genericType = controller.ControllerType.GenericTypeArguments[0];
+ var queryDiscovery = this.serviceProvider.GetRequiredService();
+ var query = queryDiscovery.FindQuery(genericType);
+ controller.ControllerName = $"{query.LowerCamelCaseName}";
+ }
+ }
+ }
+}
diff --git a/PoweredSoft.CQRS.AspNetCore.OData/QueryODataControllerFeatureProvider.cs b/PoweredSoft.CQRS.AspNetCore.OData/QueryODataControllerFeatureProvider.cs
new file mode 100644
index 0000000..9ad2936
--- /dev/null
+++ b/PoweredSoft.CQRS.AspNetCore.OData/QueryODataControllerFeatureProvider.cs
@@ -0,0 +1,45 @@
+using Microsoft.AspNetCore.Mvc.ApplicationParts;
+using Microsoft.AspNetCore.Mvc.Controllers;
+using Microsoft.Extensions.DependencyInjection;
+using PoweredSoft.CQRS.Abstractions.Discovery;
+using PoweredSoft.CQRS.AspNetCore.Abstractions.Attributes;
+using PoweredSoft.CQRS.AspNetCore.OData.Abstractions;
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+using System.Text;
+
+namespace PoweredSoft.CQRS.AspNetCore.OData
+{
+ public class QueryODataControllerFeatureProvider : IApplicationFeatureProvider
+ {
+ private readonly ServiceProvider serviceProvider;
+
+ public QueryODataControllerFeatureProvider(ServiceProvider serviceProvider)
+ {
+ this.serviceProvider = serviceProvider;
+ }
+
+ public void PopulateFeature(IEnumerable parts, ControllerFeature feature)
+ {
+ var queryDiscovery = this.serviceProvider.GetRequiredService();
+ foreach (var f in queryDiscovery.GetQueries())
+ {
+ var ignoreAttribute = f.QueryType.GetCustomAttribute();
+ if (ignoreAttribute != null)
+ continue;
+
+ if (f.Category != "BasicQuery")
+ continue;
+
+ var isQueryable = f.QueryResultType.Namespace == "System.Linq" && f.QueryResultType.Name.Contains("IQueryable");
+ if (!isQueryable)
+ continue;
+
+ var controllerType = typeof(QueryODataController<,>).MakeGenericType(f.QueryType, f.QueryResultType);
+ var controllerTypeInfo = controllerType.GetTypeInfo();
+ feature.Controllers.Add(controllerTypeInfo);
+ }
+ }
+ }
+}
diff --git a/PoweredSoft.CQRS.AspNetCore.OData/QueryODataControllerOptions.cs b/PoweredSoft.CQRS.AspNetCore.OData/QueryODataControllerOptions.cs
new file mode 100644
index 0000000..031cec8
--- /dev/null
+++ b/PoweredSoft.CQRS.AspNetCore.OData/QueryODataControllerOptions.cs
@@ -0,0 +1,11 @@
+namespace PoweredSoft.CQRS.AspNetCore.Mvc
+{
+ public class QueryODataControllerOptions
+ {
+ public bool FixODataSwagger { get; set; } = true;
+
+ public QueryODataControllerOptions()
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/PoweredSoft.CQRS.sln b/PoweredSoft.CQRS.sln
index a483732..84353c2 100644
--- a/PoweredSoft.CQRS.sln
+++ b/PoweredSoft.CQRS.sln
@@ -35,6 +35,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PoweredSoft.CQRS.GraphQL.Ho
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PoweredSoft.CQRS.GraphQL.DynamicQuery", "PoweredSoft.CQRS.GraphQL.DynamicQuery\PoweredSoft.CQRS.GraphQL.DynamicQuery.csproj", "{34B27880-A5D5-47EA-A5FA-86E04E0F7A21}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PoweredSoft.CQRS.AspNetCore.OData", "PoweredSoft.CQRS.AspNetCore.OData\PoweredSoft.CQRS.AspNetCore.OData.csproj", "{04459C2D-B02F-4FF0-8D66-73042F27C7CC}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PoweredSoft.CQRS.AspNetCore.OData.Abstractions", "PoweredSoft.CQRS.AspNetCore.OData.Abstractions\PoweredSoft.CQRS.AspNetCore.OData.Abstractions.csproj", "{9B65B727-C088-4562-A607-8BD5B5EFF289}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -93,6 +97,14 @@ Global
{34B27880-A5D5-47EA-A5FA-86E04E0F7A21}.Debug|Any CPU.Build.0 = Debug|Any CPU
{34B27880-A5D5-47EA-A5FA-86E04E0F7A21}.Release|Any CPU.ActiveCfg = Release|Any CPU
{34B27880-A5D5-47EA-A5FA-86E04E0F7A21}.Release|Any CPU.Build.0 = Release|Any CPU
+ {04459C2D-B02F-4FF0-8D66-73042F27C7CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {04459C2D-B02F-4FF0-8D66-73042F27C7CC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {04459C2D-B02F-4FF0-8D66-73042F27C7CC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {04459C2D-B02F-4FF0-8D66-73042F27C7CC}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9B65B727-C088-4562-A607-8BD5B5EFF289}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9B65B727-C088-4562-A607-8BD5B5EFF289}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9B65B727-C088-4562-A607-8BD5B5EFF289}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9B65B727-C088-4562-A607-8BD5B5EFF289}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE