From 7a67866dc397a47a3600d4af052de13f8088ad97 Mon Sep 17 00:00:00 2001 From: David Lebee Date: Fri, 5 Feb 2021 13:06:34 -0500 Subject: [PATCH] OData support. :) --- Demo/Demo.csproj | 2 + Demo/Startup.cs | 10 +++- ....CQRS.AspNetCore.OData.Abstractions.csproj | 7 +++ .../QueryControllerIgnoreAttribute.cs | 11 +++++ .../EndpointExstensions.cs | 44 ++++++++++++++++++ .../MvcBuilderExensions.cs | 46 +++++++++++++++++++ .../PoweredSoft.CQRS.AspNetCore.OData.csproj | 21 +++++++++ .../QueryODataController.cs | 23 ++++++++++ .../QueryODataControllerConvention.cs | 29 ++++++++++++ .../QueryODataControllerFeatureProvider.cs | 45 ++++++++++++++++++ .../QueryODataControllerOptions.cs | 11 +++++ PoweredSoft.CQRS.sln | 12 +++++ 12 files changed, 259 insertions(+), 2 deletions(-) create mode 100644 PoweredSoft.CQRS.AspNetCore.OData.Abstractions/PoweredSoft.CQRS.AspNetCore.OData.Abstractions.csproj create mode 100644 PoweredSoft.CQRS.AspNetCore.OData.Abstractions/QueryControllerIgnoreAttribute.cs create mode 100644 PoweredSoft.CQRS.AspNetCore.OData/EndpointExstensions.cs create mode 100644 PoweredSoft.CQRS.AspNetCore.OData/MvcBuilderExensions.cs create mode 100644 PoweredSoft.CQRS.AspNetCore.OData/PoweredSoft.CQRS.AspNetCore.OData.csproj create mode 100644 PoweredSoft.CQRS.AspNetCore.OData/QueryODataController.cs create mode 100644 PoweredSoft.CQRS.AspNetCore.OData/QueryODataControllerConvention.cs create mode 100644 PoweredSoft.CQRS.AspNetCore.OData/QueryODataControllerFeatureProvider.cs create mode 100644 PoweredSoft.CQRS.AspNetCore.OData/QueryODataControllerOptions.cs 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