From 3037b35f716f14ed08eb321e58f138e94f52d93b Mon Sep 17 00:00:00 2001 From: fanliang11 <137629448@qq.com> Date: Sat, 5 Feb 2022 16:56:05 +0800 Subject: [PATCH] Upgrade .net 6.0 and support swagger 5.0 --- .../DotNetty.Codecs.DNS.csproj | 2 +- .../Surging.ApiGateway.csproj | 2 +- .../Surging.Apm.Skywalking.csproj | 2 +- .../Surging.Core.Abp/Surging.Core.Abp.csproj | 2 +- .../Surging.Core.ApiGateWay.csproj | 2 +- .../Surging.Core.AutoMapper.csproj | 2 +- .../Implementation/ClrServiceEntryFactory.cs | 1 + .../Runtime/Server/ServiceEntry.cs | 2 + .../Surging.Core.CPlatform.csproj | 2 +- .../Surging.Core.Caching.csproj | 2 +- .../Surging.Core.Codec.MessagePack.csproj | 2 +- .../Surging.Core.Codec.ProtoBuffer.csproj | 2 +- .../Surging.Core.Common.csproj | 2 +- .../Surging.Core.Configuration.Apollo.csproj | 2 +- .../Surging.Core.Consul.csproj | 2 +- .../Surging.Core.DNS/Surging.Core.DNS.csproj | 2 +- .../DotNettyServerMessageListener.cs | 7 +- .../Surging.Core.DotNetty.csproj | 2 +- .../Surging.Core.DotNettyWSServer.csproj | 2 +- .../Surging.Core.EventBusKafka.csproj | 2 +- .../Surging.Core.EventBusRabbitMQ.csproj | 2 +- .../Surging.Core.Grpc.csproj | 2 +- .../Surging.Core.Kestrel.Log4net.csproj | 2 +- .../Surging.Core.Kestrel.Nlog.csproj | 2 +- .../Internal/DateTimeConverter.cs | 28 + .../Internal/StreamCopyOperation.cs | 2 +- .../KestrelHttpMessageListener.cs | 4 +- .../Surging.Core.KestrelHttpServer.csproj | 19 +- .../Surging.Core.Log4net.csproj | 2 +- .../Surging.Core.Nlog.csproj | 2 +- .../DotNettyHttpServerMessageListener.cs | 1 + .../Surging.Core.Protocol.Http.csproj | 2 +- .../Surging.Core.Protocol.Mqtt.csproj | 2 +- .../Surging.Core.Protocol.Udp.csproj | 2 +- .../Surging.Core.Protocol.WS.csproj | 2 +- .../Implementation/ServiceProxyGenerater.cs | 2 +- .../Surging.Core.ProxyGenerator.csproj | 2 +- .../Surging.Core.Serilog.csproj | 2 +- ...ging.Core.ServiceHosting.Extensions.csproj | 2 +- .../Surging.Core.ServiceHosting.csproj | 2 +- .../Surging.Core.Stage/StageModule.cs | 9 +- .../Surging.Core.Stage.csproj | 2 +- .../Surging.Core.Swagger.csproj | 2 +- .../Surging.Core.Swagger_V5/AppConfig.cs | 20 + .../Internal/DefaultServiceSchemaProvider.cs | 36 + .../Internal/IServiceSchemaProvider.cs | 11 + .../Surging.Core.Swagger_V5.csproj | 67 ++ .../SwaggerBuilderExtensions.cs | 73 ++ .../AddAuthorizationOperationFilter.cs | 70 ++ .../Swagger/ISwaggerProvider.cs | 24 + .../Swagger/Model/SecurityScheme.cs | 20 + .../Swagger/Model/SwaggerDocument.cs | 401 ++++++++ .../Swagger/SwaggerEndpointOptions.cs | 29 + .../Swagger/SwaggerMiddleware.cs | 113 +++ .../Swagger/SwaggerOptions.cs | 32 + .../ConfigureSchemaGeneratorOptions.cs | 54 + .../ConfigureSwaggerGeneratorOptions.cs | 82 ++ .../DependencyInjection/DocumentProvider.cs | 60 ++ .../SwaggerApplicationConvention.cs | 12 + .../DependencyInjection/SwaggerGenOptions.cs | 34 + .../SwaggerGenOptionsExtensions.cs | 537 ++++++++++ .../SwaggerGenServiceCollectionExtensions.cs | 60 ++ .../SchemaGenerator/ISchemaFilter.cs | 40 + .../ISerializerDataContractResolver.cs | 179 ++++ .../JsonSerializerDataContractResolver.cs | 247 +++++ .../SchemaGenerator/MemberInfoExtensions.cs | 101 ++ .../OpenApiSchemaExtensions.cs | 113 +++ .../SchemaGenerator/PropertyInfoExtensions.cs | 24 + .../SchemaGenerator/SchemaGenerator.cs | 475 +++++++++ .../SchemaGenerator/SchemaGeneratorOptions.cs | 72 ++ .../SchemaGenerator/TypeExtensions.cs | 96 ++ .../ApiDescriptionExtensions.cs | 61 ++ .../ApiParameterDescriptionExtensions.cs | 112 +++ .../ApiResponseTypeExtensions.cs | 19 + .../SwaggerGenerator/IDictionary.cs | 6 + .../SwaggerGenerator/IDocumentFilter.cs | 32 + .../SwaggerGenerator/IFileResult.cs | 6 + .../SwaggerGenerator/IOperationFilter.cs | 48 + .../SwaggerGenerator/IParameterFilter.cs | 40 + .../SwaggerGenerator/IRequestBodyFilter.cs | 36 + .../SwaggerGenerator/ISchemaGenerator.cs | 15 + .../SwaggerGenerator/OpenApiAnyFactory.cs | 80 ++ .../SwaggerGenerator/SchemaRepository.cs | 60 ++ .../SwaggerGenerator/StringExtensions.cs | 17 + .../SwaggerGenerator/SwaggerGenerator.cs | 925 ++++++++++++++++++ .../SwaggerGeneratorException.cs | 13 + .../SwaggerGeneratorOptions.cs | 137 +++ .../XmlComments/MethodInfoExtensions.cs | 26 + .../XmlComments/XmlCommentsDocumentFilter.cs | 54 + .../XmlComments/XmlCommentsNodeNameHelper.cs | 99 ++ .../XmlComments/XmlCommentsOperationFilter.cs | 71 ++ .../XmlComments/XmlCommentsParameterFilter.cs | 82 ++ .../XmlCommentsRequestBodyFilter.cs | 94 ++ .../XmlComments/XmlCommentsSchemaFilter.cs | 59 ++ .../XmlComments/XmlCommentsTextHelper.cs | 116 +++ .../Surging.Core.Swagger_V5/SwaggerModule.cs | 113 +++ .../SwaggerUI/SwaggerUIBuilderExtensions.cs | 48 + .../SwaggerUI/SwaggerUIMiddleware.cs | 133 +++ .../SwaggerUI/SwaggerUIOptions.cs | 249 +++++ .../SwaggerUI/SwaggerUIOptionsExtensions.cs | 368 +++++++ .../SwaggerUI/index.html | 117 +++ .../Surging.Core.System.csproj | 2 +- .../Surging.Core.Thrift.csproj | 2 +- .../Surging.Core.Zookeeper.csproj | 2 +- .../Surging.IModuleServices.Common.csproj | 2 +- .../Surging.IModuleServices.Manager.csproj | 2 +- .../Surging.Modules.Common.csproj | 2 +- .../Surging.Modules.Manager.csproj | 4 +- .../Surging.Services.Client.csproj | 2 +- .../Surging.Services.Server.csproj | 4 +- .../Surging.Tools.Cli.csproj | 2 +- src/Surging.Web/Surging.Web.csproj | 2 +- src/Surging.sln | 20 +- .../WebSocketCore/WebSocketCore.csproj | 2 +- 114 files changed, 6423 insertions(+), 84 deletions(-) create mode 100644 src/Surging.Core/Surging.Core.KestrelHttpServer/Internal/DateTimeConverter.cs create mode 100644 src/Surging.Core/Surging.Core.Swagger_V5/AppConfig.cs create mode 100644 src/Surging.Core/Surging.Core.Swagger_V5/Internal/DefaultServiceSchemaProvider.cs create mode 100644 src/Surging.Core/Surging.Core.Swagger_V5/Internal/IServiceSchemaProvider.cs create mode 100644 src/Surging.Core/Surging.Core.Swagger_V5/Surging.Core.Swagger_V5.csproj create mode 100644 src/Surging.Core/Surging.Core.Swagger_V5/Swagger/DependencyInjection/SwaggerBuilderExtensions.cs create mode 100644 src/Surging.Core/Surging.Core.Swagger_V5/Swagger/Filters/AddAuthorizationOperationFilter.cs create mode 100644 src/Surging.Core/Surging.Core.Swagger_V5/Swagger/ISwaggerProvider.cs create mode 100644 src/Surging.Core/Surging.Core.Swagger_V5/Swagger/Model/SecurityScheme.cs create mode 100644 src/Surging.Core/Surging.Core.Swagger_V5/Swagger/Model/SwaggerDocument.cs create mode 100644 src/Surging.Core/Surging.Core.Swagger_V5/Swagger/SwaggerEndpointOptions.cs create mode 100644 src/Surging.Core/Surging.Core.Swagger_V5/Swagger/SwaggerMiddleware.cs create mode 100644 src/Surging.Core/Surging.Core.Swagger_V5/Swagger/SwaggerOptions.cs create mode 100644 src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/DependencyInjection/ConfigureSchemaGeneratorOptions.cs create mode 100644 src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/DependencyInjection/ConfigureSwaggerGeneratorOptions.cs create mode 100644 src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/DependencyInjection/DocumentProvider.cs create mode 100644 src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/DependencyInjection/SwaggerApplicationConvention.cs create mode 100644 src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/DependencyInjection/SwaggerGenOptions.cs create mode 100644 src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/DependencyInjection/SwaggerGenOptionsExtensions.cs create mode 100644 src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/DependencyInjection/SwaggerGenServiceCollectionExtensions.cs create mode 100644 src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SchemaGenerator/ISchemaFilter.cs create mode 100644 src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SchemaGenerator/ISerializerDataContractResolver.cs create mode 100644 src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SchemaGenerator/JsonSerializerDataContractResolver.cs create mode 100644 src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SchemaGenerator/MemberInfoExtensions.cs create mode 100644 src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SchemaGenerator/OpenApiSchemaExtensions.cs create mode 100644 src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SchemaGenerator/PropertyInfoExtensions.cs create mode 100644 src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SchemaGenerator/SchemaGenerator.cs create mode 100644 src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SchemaGenerator/SchemaGeneratorOptions.cs create mode 100644 src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SchemaGenerator/TypeExtensions.cs create mode 100644 src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/ApiDescriptionExtensions.cs create mode 100644 src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/ApiParameterDescriptionExtensions.cs create mode 100644 src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/ApiResponseTypeExtensions.cs create mode 100644 src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/IDictionary.cs create mode 100644 src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/IDocumentFilter.cs create mode 100644 src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/IFileResult.cs create mode 100644 src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/IOperationFilter.cs create mode 100644 src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/IParameterFilter.cs create mode 100644 src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/IRequestBodyFilter.cs create mode 100644 src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/ISchemaGenerator.cs create mode 100644 src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/OpenApiAnyFactory.cs create mode 100644 src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/SchemaRepository.cs create mode 100644 src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/StringExtensions.cs create mode 100644 src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs create mode 100644 src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/SwaggerGeneratorException.cs create mode 100644 src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/SwaggerGeneratorOptions.cs create mode 100644 src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/XmlComments/MethodInfoExtensions.cs create mode 100644 src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/XmlComments/XmlCommentsDocumentFilter.cs create mode 100644 src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/XmlComments/XmlCommentsNodeNameHelper.cs create mode 100644 src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/XmlComments/XmlCommentsOperationFilter.cs create mode 100644 src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/XmlComments/XmlCommentsParameterFilter.cs create mode 100644 src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/XmlComments/XmlCommentsRequestBodyFilter.cs create mode 100644 src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/XmlComments/XmlCommentsSchemaFilter.cs create mode 100644 src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/XmlComments/XmlCommentsTextHelper.cs create mode 100644 src/Surging.Core/Surging.Core.Swagger_V5/SwaggerModule.cs create mode 100644 src/Surging.Core/Surging.Core.Swagger_V5/SwaggerUI/SwaggerUIBuilderExtensions.cs create mode 100644 src/Surging.Core/Surging.Core.Swagger_V5/SwaggerUI/SwaggerUIMiddleware.cs create mode 100644 src/Surging.Core/Surging.Core.Swagger_V5/SwaggerUI/SwaggerUIOptions.cs create mode 100644 src/Surging.Core/Surging.Core.Swagger_V5/SwaggerUI/SwaggerUIOptionsExtensions.cs create mode 100644 src/Surging.Core/Surging.Core.Swagger_V5/SwaggerUI/index.html diff --git a/src/DotNetty.Codecs/DotNetty.Codecs.DNS/DotNetty.Codecs.DNS.csproj b/src/DotNetty.Codecs/DotNetty.Codecs.DNS/DotNetty.Codecs.DNS.csproj index 30966d43c..ab99a9e77 100644 --- a/src/DotNetty.Codecs/DotNetty.Codecs.DNS/DotNetty.Codecs.DNS.csproj +++ b/src/DotNetty.Codecs/DotNetty.Codecs.DNS/DotNetty.Codecs.DNS.csproj @@ -1,7 +1,7 @@  - netstandard1.3;net45 + netstandard2.0;net472 false 1.6.1 diff --git a/src/Surging.ApiGateway/Surging.ApiGateway.csproj b/src/Surging.ApiGateway/Surging.ApiGateway.csproj index 4e33fe08b..527d3b1b9 100644 --- a/src/Surging.ApiGateway/Surging.ApiGateway.csproj +++ b/src/Surging.ApiGateway/Surging.ApiGateway.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 diff --git a/src/Surging.Apm/Surging.Apm.Skywalking/Surging.Apm.Skywalking.csproj b/src/Surging.Apm/Surging.Apm.Skywalking/Surging.Apm.Skywalking.csproj index 496fc6a01..bde2133dd 100644 --- a/src/Surging.Apm/Surging.Apm.Skywalking/Surging.Apm.Skywalking.csproj +++ b/src/Surging.Apm/Surging.Apm.Skywalking/Surging.Apm.Skywalking.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 diff --git a/src/Surging.Core/Surging.Core.Abp/Surging.Core.Abp.csproj b/src/Surging.Core/Surging.Core.Abp/Surging.Core.Abp.csproj index ba9a6c9ab..6040ec552 100644 --- a/src/Surging.Core/Surging.Core.Abp/Surging.Core.Abp.csproj +++ b/src/Surging.Core/Surging.Core.Abp/Surging.Core.Abp.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 diff --git a/src/Surging.Core/Surging.Core.ApiGateWay/Surging.Core.ApiGateWay.csproj b/src/Surging.Core/Surging.Core.ApiGateWay/Surging.Core.ApiGateWay.csproj index 98ed4d562..d7775cfc2 100644 --- a/src/Surging.Core/Surging.Core.ApiGateWay/Surging.Core.ApiGateWay.csproj +++ b/src/Surging.Core/Surging.Core.ApiGateWay/Surging.Core.ApiGateWay.csproj @@ -1,7 +1,7 @@ - netcoreapp3.1 + net6.0 1.0.0.0 fanly surging Micro Service Framework diff --git a/src/Surging.Core/Surging.Core.AutoMapper/Surging.Core.AutoMapper.csproj b/src/Surging.Core/Surging.Core.AutoMapper/Surging.Core.AutoMapper.csproj index c36f84d1f..fa5664335 100644 --- a/src/Surging.Core/Surging.Core.AutoMapper/Surging.Core.AutoMapper.csproj +++ b/src/Surging.Core/Surging.Core.AutoMapper/Surging.Core.AutoMapper.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 通过AutoMapper组件实现Dto对象和实体对象之间的相互转换 diff --git a/src/Surging.Core/Surging.Core.CPlatform/Runtime/Server/Implementation/ServiceDiscovery/Implementation/ClrServiceEntryFactory.cs b/src/Surging.Core/Surging.Core.CPlatform/Runtime/Server/Implementation/ServiceDiscovery/Implementation/ClrServiceEntryFactory.cs index a255f50e5..03c915736 100644 --- a/src/Surging.Core/Surging.Core.CPlatform/Runtime/Server/Implementation/ServiceDiscovery/Implementation/ClrServiceEntryFactory.cs +++ b/src/Surging.Core/Surging.Core.CPlatform/Runtime/Server/Implementation/ServiceDiscovery/Implementation/ClrServiceEntryFactory.cs @@ -114,6 +114,7 @@ private ServiceEntry Create(MethodInfo method, string serviceName, string routeT RoutePath = serviceDescriptor.RoutePath, Methods=httpMethods, MethodName = method.Name, + Parameters = method.GetParameters(), Type = method.DeclaringType, Attributes = attributes, Func = (key, parameters) => diff --git a/src/Surging.Core/Surging.Core.CPlatform/Runtime/Server/ServiceEntry.cs b/src/Surging.Core/Surging.Core.CPlatform/Runtime/Server/ServiceEntry.cs index 97cea1b32..482df48c5 100644 --- a/src/Surging.Core/Surging.Core.CPlatform/Runtime/Server/ServiceEntry.cs +++ b/src/Surging.Core/Surging.Core.CPlatform/Runtime/Server/ServiceEntry.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Reflection; using System.Threading.Tasks; namespace Surging.Core.CPlatform.Runtime.Server @@ -17,6 +18,7 @@ public class ServiceEntry public string RoutePath { get; set; } public Type Type { get; set; } public string MethodName { get; set; } + public ParameterInfo[] Parameters { get; set; } public List Attributes { get; set; } /// /// 服务描述符。 diff --git a/src/Surging.Core/Surging.Core.CPlatform/Surging.Core.CPlatform.csproj b/src/Surging.Core/Surging.Core.CPlatform/Surging.Core.CPlatform.csproj index ae0015439..fa9dbb049 100644 --- a/src/Surging.Core/Surging.Core.CPlatform/Surging.Core.CPlatform.csproj +++ b/src/Surging.Core/Surging.Core.CPlatform/Surging.Core.CPlatform.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 false surging fanly diff --git a/src/Surging.Core/Surging.Core.Caching/Surging.Core.Caching.csproj b/src/Surging.Core/Surging.Core.Caching/Surging.Core.Caching.csproj index a65f2b1b0..6eb2c7ce1 100644 --- a/src/Surging.Core/Surging.Core.Caching/Surging.Core.Caching.csproj +++ b/src/Surging.Core/Surging.Core.Caching/Surging.Core.Caching.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 1.1.0.0 fanly surging Micro Service Framework diff --git a/src/Surging.Core/Surging.Core.Codec.MessagePack/Surging.Core.Codec.MessagePack.csproj b/src/Surging.Core/Surging.Core.Codec.MessagePack/Surging.Core.Codec.MessagePack.csproj index 414fec551..fd759e630 100644 --- a/src/Surging.Core/Surging.Core.Codec.MessagePack/Surging.Core.Codec.MessagePack.csproj +++ b/src/Surging.Core/Surging.Core.Codec.MessagePack/Surging.Core.Codec.MessagePack.csproj @@ -1,7 +1,7 @@ - netcoreapp3.1 + net6.0 1.1.0.0 fanly surging Micro Service Framework diff --git a/src/Surging.Core/Surging.Core.Codec.ProtoBuffer/Surging.Core.Codec.ProtoBuffer.csproj b/src/Surging.Core/Surging.Core.Codec.ProtoBuffer/Surging.Core.Codec.ProtoBuffer.csproj index efd4c6558..2c4ba86f8 100644 --- a/src/Surging.Core/Surging.Core.Codec.ProtoBuffer/Surging.Core.Codec.ProtoBuffer.csproj +++ b/src/Surging.Core/Surging.Core.Codec.ProtoBuffer/Surging.Core.Codec.ProtoBuffer.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 surging is a distributed micro service framework that provides high-performance RPC remote service calls, using Zookeeper, Consul as the registration center for surging services, integrating hash, random, polling as a load balancing algorithm, RPC integration using the netty framework, Using asynchronous transmission. Use json.net, protobuf, messagepack for serialization Codec 1.1.0.0 fanly diff --git a/src/Surging.Core/Surging.Core.Common/Surging.Core.Common.csproj b/src/Surging.Core/Surging.Core.Common/Surging.Core.Common.csproj index 8f8c490fc..f1727e286 100644 --- a/src/Surging.Core/Surging.Core.Common/Surging.Core.Common.csproj +++ b/src/Surging.Core/Surging.Core.Common/Surging.Core.Common.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 diff --git a/src/Surging.Core/Surging.Core.Configuration.Apollo/Surging.Core.Configuration.Apollo.csproj b/src/Surging.Core/Surging.Core.Configuration.Apollo/Surging.Core.Configuration.Apollo.csproj index e96b1c20d..427dfb261 100644 --- a/src/Surging.Core/Surging.Core.Configuration.Apollo/Surging.Core.Configuration.Apollo.csproj +++ b/src/Surging.Core/Surging.Core.Configuration.Apollo/Surging.Core.Configuration.Apollo.csproj @@ -1,7 +1,7 @@ - netcoreapp3.1 + net6.0 diff --git a/src/Surging.Core/Surging.Core.Consul/Surging.Core.Consul.csproj b/src/Surging.Core/Surging.Core.Consul/Surging.Core.Consul.csproj index bfd19cf10..d9fc55559 100644 --- a/src/Surging.Core/Surging.Core.Consul/Surging.Core.Consul.csproj +++ b/src/Surging.Core/Surging.Core.Consul/Surging.Core.Consul.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 fanly 1.1.0.0 surging Micro Service Framework diff --git a/src/Surging.Core/Surging.Core.DNS/Surging.Core.DNS.csproj b/src/Surging.Core/Surging.Core.DNS/Surging.Core.DNS.csproj index 073e9e707..68f720fd8 100644 --- a/src/Surging.Core/Surging.Core.DNS/Surging.Core.DNS.csproj +++ b/src/Surging.Core/Surging.Core.DNS/Surging.Core.DNS.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 diff --git a/src/Surging.Core/Surging.Core.DotNetty/DotNettyServerMessageListener.cs b/src/Surging.Core/Surging.Core.DotNetty/DotNettyServerMessageListener.cs index 11c208211..0b813e141 100644 --- a/src/Surging.Core/Surging.Core.DotNetty/DotNettyServerMessageListener.cs +++ b/src/Surging.Core/Surging.Core.DotNetty/DotNettyServerMessageListener.cs @@ -78,7 +78,8 @@ public async Task StartAsync(EndPoint endPoint) bossGroup = new MultithreadEventLoopGroup(1); workerGroup = new MultithreadEventLoopGroup(); bootstrap.Channel(); - } + } + var workerGroup1 = new SingleThreadEventLoop(); bootstrap .Option(ChannelOption.SoBacklog, AppConfig.ServerOptions.SoBacklog) .ChildOption(ChannelOption.Allocator, PooledByteBufferAllocator.Default) @@ -88,8 +89,8 @@ public async Task StartAsync(EndPoint endPoint) var pipeline = channel.Pipeline; pipeline.AddLast(new LengthFieldPrepender(4)); pipeline.AddLast(new LengthFieldBasedFrameDecoder(int.MaxValue, 0, 4, 0, 4)); - pipeline.AddLast(workerGroup, "HandlerAdapter", new TransportMessageChannelHandlerAdapter(_transportMessageDecoder)); - pipeline.AddLast(workerGroup, "ServerHandler", new ServerHandler(async (contenxt, message) => + pipeline.AddLast(workerGroup1, "HandlerAdapter", new TransportMessageChannelHandlerAdapter(_transportMessageDecoder)); + pipeline.AddLast(workerGroup1, "ServerHandler", new ServerHandler(async (contenxt, message) => { var sender = new DotNettyServerMessageSender(_transportMessageEncoder, contenxt); await OnReceived(sender, message); diff --git a/src/Surging.Core/Surging.Core.DotNetty/Surging.Core.DotNetty.csproj b/src/Surging.Core/Surging.Core.DotNetty/Surging.Core.DotNetty.csproj index 798f4202f..15ca46fde 100644 --- a/src/Surging.Core/Surging.Core.DotNetty/Surging.Core.DotNetty.csproj +++ b/src/Surging.Core/Surging.Core.DotNetty/Surging.Core.DotNetty.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 1.1.0.0 surging Micro Service Framework diff --git a/src/Surging.Core/Surging.Core.DotNettyWSServer/Surging.Core.DotNettyWSServer.csproj b/src/Surging.Core/Surging.Core.DotNettyWSServer/Surging.Core.DotNettyWSServer.csproj index dbc74560c..d0849dd1d 100644 --- a/src/Surging.Core/Surging.Core.DotNettyWSServer/Surging.Core.DotNettyWSServer.csproj +++ b/src/Surging.Core/Surging.Core.DotNettyWSServer/Surging.Core.DotNettyWSServer.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 diff --git a/src/Surging.Core/Surging.Core.EventBusKafka/Surging.Core.EventBusKafka.csproj b/src/Surging.Core/Surging.Core.EventBusKafka/Surging.Core.EventBusKafka.csproj index f7c374659..f49ac7f50 100644 --- a/src/Surging.Core/Surging.Core.EventBusKafka/Surging.Core.EventBusKafka.csproj +++ b/src/Surging.Core/Surging.Core.EventBusKafka/Surging.Core.EventBusKafka.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 1.1.0.0 fanly fanly diff --git a/src/Surging.Core/Surging.Core.EventBusRabbitMQ/Surging.Core.EventBusRabbitMQ.csproj b/src/Surging.Core/Surging.Core.EventBusRabbitMQ/Surging.Core.EventBusRabbitMQ.csproj index a40d9c6d3..6c8b38f31 100644 --- a/src/Surging.Core/Surging.Core.EventBusRabbitMQ/Surging.Core.EventBusRabbitMQ.csproj +++ b/src/Surging.Core/Surging.Core.EventBusRabbitMQ/Surging.Core.EventBusRabbitMQ.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 surging is a distributed micro service framework that provides high-performance RPC remote service calls, using Zookeeper, Consul as the registration center for surging services, integrating hash, random, polling as a load balancing algorithm, RPC integration using the netty framework, Using asynchronous transmission. Use json.net, protobuf, messagepack for serialization Codec 1.1.0.0 fanly diff --git a/src/Surging.Core/Surging.Core.Grpc/Surging.Core.Grpc.csproj b/src/Surging.Core/Surging.Core.Grpc/Surging.Core.Grpc.csproj index e73290751..a9530bf8c 100644 --- a/src/Surging.Core/Surging.Core.Grpc/Surging.Core.Grpc.csproj +++ b/src/Surging.Core/Surging.Core.Grpc/Surging.Core.Grpc.csproj @@ -1,7 +1,7 @@ - netcoreapp3.1 + net6.0 diff --git a/src/Surging.Core/Surging.Core.Kestrel.Log4net/Surging.Core.Kestrel.Log4net.csproj b/src/Surging.Core/Surging.Core.Kestrel.Log4net/Surging.Core.Kestrel.Log4net.csproj index defa6107d..3baee7a8e 100644 --- a/src/Surging.Core/Surging.Core.Kestrel.Log4net/Surging.Core.Kestrel.Log4net.csproj +++ b/src/Surging.Core/Surging.Core.Kestrel.Log4net/Surging.Core.Kestrel.Log4net.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 diff --git a/src/Surging.Core/Surging.Core.Kestrel.Nlog/Surging.Core.Kestrel.Nlog.csproj b/src/Surging.Core/Surging.Core.Kestrel.Nlog/Surging.Core.Kestrel.Nlog.csproj index 7385d3205..249d51b9b 100644 --- a/src/Surging.Core/Surging.Core.Kestrel.Nlog/Surging.Core.Kestrel.Nlog.csproj +++ b/src/Surging.Core/Surging.Core.Kestrel.Nlog/Surging.Core.Kestrel.Nlog.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 diff --git a/src/Surging.Core/Surging.Core.KestrelHttpServer/Internal/DateTimeConverter.cs b/src/Surging.Core/Surging.Core.KestrelHttpServer/Internal/DateTimeConverter.cs new file mode 100644 index 000000000..f0d3ae93c --- /dev/null +++ b/src/Surging.Core/Surging.Core.KestrelHttpServer/Internal/DateTimeConverter.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace Surging.Core.KestrelHttpServer.Internal +{ + public class DateTimeNullConverter : JsonConverter + { + public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => string.IsNullOrEmpty(reader.GetString()) ? default(DateTime?) : DateTime.Parse(reader.GetString()); + + public override void Write(Utf8JsonWriter writer, DateTime? value, JsonSerializerOptions options) + => writer.WriteStringValue(value?.ToString("yyyy-MM-dd HH:mm")); + } + + public class DateTimeConverter : JsonConverter + { + public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => DateTime.Parse(reader.GetString()); + + public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) + => writer.WriteStringValue(value.ToString("yyyy-MM-dd HH:mm")); + } +} diff --git a/src/Surging.Core/Surging.Core.KestrelHttpServer/Internal/StreamCopyOperation.cs b/src/Surging.Core/Surging.Core.KestrelHttpServer/Internal/StreamCopyOperation.cs index 93ae2542a..b01886a86 100644 --- a/src/Surging.Core/Surging.Core.KestrelHttpServer/Internal/StreamCopyOperation.cs +++ b/src/Surging.Core/Surging.Core.KestrelHttpServer/Internal/StreamCopyOperation.cs @@ -93,7 +93,7 @@ public static async Task CopyToAsync(Stream source, Stream destination, long? co cancel.ThrowIfCancellationRequested(); - destination.Write(buffer, 0, read); + await destination.WriteAsync(buffer, 0, read); } } finally diff --git a/src/Surging.Core/Surging.Core.KestrelHttpServer/KestrelHttpMessageListener.cs b/src/Surging.Core/Surging.Core.KestrelHttpServer/KestrelHttpMessageListener.cs index b623612f4..50c4cdc53 100644 --- a/src/Surging.Core/Surging.Core.KestrelHttpServer/KestrelHttpMessageListener.cs +++ b/src/Surging.Core/Surging.Core.KestrelHttpServer/KestrelHttpMessageListener.cs @@ -55,7 +55,7 @@ public KestrelHttpMessageListener(ILogger logger, _moduleProvider = moduleProvider; _container = container; _serviceRouteProvider = serviceRouteProvider; - _diagnosticListener = new DiagnosticListener(DiagnosticListenerExtensions.DiagnosticListenerName); + _diagnosticListener = new DiagnosticListener(CPlatform.Diagnostics.DiagnosticListenerExtensions.DiagnosticListenerName); } public async Task StartAsync(IPAddress address,int? port) @@ -115,7 +115,7 @@ public void ConfigureHost(WebHostBuilderContext context, KestrelServerOptions op public void ConfigureServices(IServiceCollection services) { var builder = new ContainerBuilder(); - services.AddMvc(); + services.AddMvc(option => option.EnableEndpointRouting = false); _moduleProvider.ConfigureServices(new ConfigurationContext(services, _moduleProvider.Modules, _moduleProvider.VirtualPaths, diff --git a/src/Surging.Core/Surging.Core.KestrelHttpServer/Surging.Core.KestrelHttpServer.csproj b/src/Surging.Core/Surging.Core.KestrelHttpServer/Surging.Core.KestrelHttpServer.csproj index ffcf77f3d..c3b21da36 100644 --- a/src/Surging.Core/Surging.Core.KestrelHttpServer/Surging.Core.KestrelHttpServer.csproj +++ b/src/Surging.Core/Surging.Core.KestrelHttpServer/Surging.Core.KestrelHttpServer.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 1.1.0.0 fanly fanly @@ -18,13 +18,7 @@ - - - - - - - + @@ -32,13 +26,4 @@ - - - C:\Program Files\dotnet\sdk\NuGetFallbackFolder\microsoft.aspnetcore.hosting\2.1.0\lib\netstandard2.0\Microsoft.AspNetCore.Hosting.dll - - - C:\Program Files\dotnet\sdk\NuGetFallbackFolder\microsoft.extensions.logging.abstractions\2.1.0\lib\netstandard2.0\Microsoft.Extensions.Logging.Abstractions.dll - - - diff --git a/src/Surging.Core/Surging.Core.Log4net/Surging.Core.Log4net.csproj b/src/Surging.Core/Surging.Core.Log4net/Surging.Core.Log4net.csproj index c9abff74c..2f8c55887 100644 --- a/src/Surging.Core/Surging.Core.Log4net/Surging.Core.Log4net.csproj +++ b/src/Surging.Core/Surging.Core.Log4net/Surging.Core.Log4net.csproj @@ -1,7 +1,7 @@ - netcoreapp3.1 + net6.0 1.1.0.0 fanly diff --git a/src/Surging.Core/Surging.Core.NLog/Surging.Core.Nlog.csproj b/src/Surging.Core/Surging.Core.NLog/Surging.Core.Nlog.csproj index d333bf96b..a70a91f8a 100644 --- a/src/Surging.Core/Surging.Core.NLog/Surging.Core.Nlog.csproj +++ b/src/Surging.Core/Surging.Core.NLog/Surging.Core.Nlog.csproj @@ -1,7 +1,7 @@ - netcoreapp3.1 + net6.0 1.multiple register center cluster 2. fix bug 1.1.0.0 diff --git a/src/Surging.Core/Surging.Core.Protocol.Http/DotNettyHttpServerMessageListener.cs b/src/Surging.Core/Surging.Core.Protocol.Http/DotNettyHttpServerMessageListener.cs index 6613d5948..90719e779 100644 --- a/src/Surging.Core/Surging.Core.Protocol.Http/DotNettyHttpServerMessageListener.cs +++ b/src/Surging.Core/Surging.Core.Protocol.Http/DotNettyHttpServerMessageListener.cs @@ -18,6 +18,7 @@ using System.Text; using System.Threading.Tasks; using System.Web; +using TaskCompletionSource = DotNetty.Common.Concurrency.TaskCompletionSource; namespace Surging.Core.Protocol.Http { diff --git a/src/Surging.Core/Surging.Core.Protocol.Http/Surging.Core.Protocol.Http.csproj b/src/Surging.Core/Surging.Core.Protocol.Http/Surging.Core.Protocol.Http.csproj index 71a4c7008..a860dc05c 100644 --- a/src/Surging.Core/Surging.Core.Protocol.Http/Surging.Core.Protocol.Http.csproj +++ b/src/Surging.Core/Surging.Core.Protocol.Http/Surging.Core.Protocol.Http.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 1.1.0.0 fanly fanly diff --git a/src/Surging.Core/Surging.Core.Protocol.Mqtt/Surging.Core.Protocol.Mqtt.csproj b/src/Surging.Core/Surging.Core.Protocol.Mqtt/Surging.Core.Protocol.Mqtt.csproj index eae6a3a3e..3d3d3c891 100644 --- a/src/Surging.Core/Surging.Core.Protocol.Mqtt/Surging.Core.Protocol.Mqtt.csproj +++ b/src/Surging.Core/Surging.Core.Protocol.Mqtt/Surging.Core.Protocol.Mqtt.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 fanly surging Micro Service Framework surging is a distributed micro service framework that provides high-performance RPC remote service calls, using Zookeeper, Consul as the registration center for surging services, integrating hash, random, polling as a load balancing algorithm, RPC integration using the netty framework, Using asynchronous transmission. Use json.net, protobuf, messagepack for serialization Codec diff --git a/src/Surging.Core/Surging.Core.Protocol.Udp/Surging.Core.Protocol.Udp.csproj b/src/Surging.Core/Surging.Core.Protocol.Udp/Surging.Core.Protocol.Udp.csproj index c96527c7f..8047e09ce 100644 --- a/src/Surging.Core/Surging.Core.Protocol.Udp/Surging.Core.Protocol.Udp.csproj +++ b/src/Surging.Core/Surging.Core.Protocol.Udp/Surging.Core.Protocol.Udp.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 diff --git a/src/Surging.Core/Surging.Core.Protocol.WS/Surging.Core.Protocol.WS.csproj b/src/Surging.Core/Surging.Core.Protocol.WS/Surging.Core.Protocol.WS.csproj index 9738fbf8c..48322d0ac 100644 --- a/src/Surging.Core/Surging.Core.Protocol.WS/Surging.Core.Protocol.WS.csproj +++ b/src/Surging.Core/Surging.Core.Protocol.WS/Surging.Core.Protocol.WS.csproj @@ -1,7 +1,7 @@ - netcoreapp3.1 + net6.0 1.1.0.0 fanly fanly diff --git a/src/Surging.Core/Surging.Core.ProxyGenerator/Implementation/ServiceProxyGenerater.cs b/src/Surging.Core/Surging.Core.ProxyGenerator/Implementation/ServiceProxyGenerater.cs index 58a756822..9e9304988 100644 --- a/src/Surging.Core/Surging.Core.ProxyGenerator/Implementation/ServiceProxyGenerater.cs +++ b/src/Surging.Core/Surging.Core.ProxyGenerator/Implementation/ServiceProxyGenerater.cs @@ -63,7 +63,7 @@ public IEnumerable GenerateProxys(IEnumerable interfacTypes, IEnumer types = interfacTypes.Except(types); foreach (var t in types) { - assemblys = assemblys.Append(t.Assembly); + assemblys = assemblys.Append(t.Assembly).ToArray(); } var trees = interfacTypes.Select(p=>GenerateProxyTree(p,namespaces)).ToList(); var stream = CompilationUtilitys.CompileClientProxy(trees, diff --git a/src/Surging.Core/Surging.Core.ProxyGenerator/Surging.Core.ProxyGenerator.csproj b/src/Surging.Core/Surging.Core.ProxyGenerator/Surging.Core.ProxyGenerator.csproj index de97c6981..406f0edf6 100644 --- a/src/Surging.Core/Surging.Core.ProxyGenerator/Surging.Core.ProxyGenerator.csproj +++ b/src/Surging.Core/Surging.Core.ProxyGenerator/Surging.Core.ProxyGenerator.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 1.1.0.0 fanly surging Micro Service Framework diff --git a/src/Surging.Core/Surging.Core.Serilog/Surging.Core.Serilog.csproj b/src/Surging.Core/Surging.Core.Serilog/Surging.Core.Serilog.csproj index 686291b0b..c636fa0b4 100644 --- a/src/Surging.Core/Surging.Core.Serilog/Surging.Core.Serilog.csproj +++ b/src/Surging.Core/Surging.Core.Serilog/Surging.Core.Serilog.csproj @@ -1,7 +1,7 @@ - netcoreapp3.1 + net6.0 diff --git a/src/Surging.Core/Surging.Core.ServiceHosting.Extensions/Surging.Core.ServiceHosting.Extensions.csproj b/src/Surging.Core/Surging.Core.ServiceHosting.Extensions/Surging.Core.ServiceHosting.Extensions.csproj index c67ceee50..e6f92d5ab 100644 --- a/src/Surging.Core/Surging.Core.ServiceHosting.Extensions/Surging.Core.ServiceHosting.Extensions.csproj +++ b/src/Surging.Core/Surging.Core.ServiceHosting.Extensions/Surging.Core.ServiceHosting.Extensions.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 diff --git a/src/Surging.Core/Surging.Core.ServiceHosting/Surging.Core.ServiceHosting.csproj b/src/Surging.Core/Surging.Core.ServiceHosting/Surging.Core.ServiceHosting.csproj index 75a037064..617392fe6 100644 --- a/src/Surging.Core/Surging.Core.ServiceHosting/Surging.Core.ServiceHosting.csproj +++ b/src/Surging.Core/Surging.Core.ServiceHosting/Surging.Core.ServiceHosting.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 Surging.Core.ServiceHosting Copyright © fanly All Rights Reserved. 1.1.0.0 diff --git a/src/Surging.Core/Surging.Core.Stage/StageModule.cs b/src/Surging.Core/Surging.Core.Stage/StageModule.cs index d0a40c840..4647565bf 100644 --- a/src/Surging.Core/Surging.Core.Stage/StageModule.cs +++ b/src/Surging.Core/Surging.Core.Stage/StageModule.cs @@ -10,6 +10,7 @@ using Surging.Core.KestrelHttpServer; using Surging.Core.KestrelHttpServer.Extensions; using Surging.Core.KestrelHttpServer.Filters; +using Surging.Core.KestrelHttpServer.Internal; using Surging.Core.Stage.Configurations; using Surging.Core.Stage.Filters; using Surging.Core.Stage.Internal; @@ -18,6 +19,7 @@ using System.Collections.Generic; using System.Net; using System.Text; +using System.Text.Json; namespace Surging.Core.Stage { @@ -67,7 +69,8 @@ public override void RegisterBuilder(ConfigurationContext context) ApiGateWay.AppConfig.TokenEndpointPath = apiConfig.TokenEndpointPath; } context.Services.AddMvc().AddJsonOptions(options => { - options.SerializerSettings.DateFormatString = "yyyy-MM-dd HH:mm:ss"; + options.JsonSerializerOptions.Converters.Add(new DateTimeConverter()); + options.JsonSerializerOptions.Converters.Add(new DateTimeNullConverter()); if (AppConfig.Options.IsCamelCaseResolver) { JsonConvert.DefaultSettings= new Func(() => @@ -77,7 +80,7 @@ public override void RegisterBuilder(ConfigurationContext context) setting.ContractResolver = new CamelCasePropertyNamesContractResolver(); return setting; }); - options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); + options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; } else { @@ -88,7 +91,7 @@ public override void RegisterBuilder(ConfigurationContext context) setting.ContractResolver= new DefaultContractResolver(); return setting; }); - options.SerializerSettings.ContractResolver = new DefaultContractResolver(); + options.JsonSerializerOptions.PropertyNamingPolicy = null; } }); diff --git a/src/Surging.Core/Surging.Core.Stage/Surging.Core.Stage.csproj b/src/Surging.Core/Surging.Core.Stage/Surging.Core.Stage.csproj index 87aecf720..5ee7869d3 100644 --- a/src/Surging.Core/Surging.Core.Stage/Surging.Core.Stage.csproj +++ b/src/Surging.Core/Surging.Core.Stage/Surging.Core.Stage.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 diff --git a/src/Surging.Core/Surging.Core.Swagger/Surging.Core.Swagger.csproj b/src/Surging.Core/Surging.Core.Swagger/Surging.Core.Swagger.csproj index 96bae0220..026c3ea68 100644 --- a/src/Surging.Core/Surging.Core.Swagger/Surging.Core.Swagger.csproj +++ b/src/Surging.Core/Surging.Core.Swagger/Surging.Core.Swagger.csproj @@ -1,7 +1,7 @@ - netcoreapp3.1 + net6.0 diff --git a/src/Surging.Core/Surging.Core.Swagger_V5/AppConfig.cs b/src/Surging.Core/Surging.Core.Swagger_V5/AppConfig.cs new file mode 100644 index 000000000..464bef6a9 --- /dev/null +++ b/src/Surging.Core/Surging.Core.Swagger_V5/AppConfig.cs @@ -0,0 +1,20 @@ +using Surging.Core.Swagger_V5.Swagger.Model; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Surging.Core.Swagger_V5 +{ + public class AppConfig + { + public static Info SwaggerOptions + { + get; internal set; + } + + public static DocumentConfiguration SwaggerConfig + { + get; internal set; + } + } +} diff --git a/src/Surging.Core/Surging.Core.Swagger_V5/Internal/DefaultServiceSchemaProvider.cs b/src/Surging.Core/Surging.Core.Swagger_V5/Internal/DefaultServiceSchemaProvider.cs new file mode 100644 index 000000000..399f67692 --- /dev/null +++ b/src/Surging.Core/Surging.Core.Swagger_V5/Internal/DefaultServiceSchemaProvider.cs @@ -0,0 +1,36 @@ +using Surging.Core.CPlatform.Engines; +using Surging.Core.CPlatform.Runtime.Server; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace Surging.Core.Swagger_V5.Internal +{ + public class DefaultServiceSchemaProvider : IServiceSchemaProvider + { + private readonly IServiceEntryProvider _serviceEntryProvider; + + public DefaultServiceSchemaProvider( IServiceEntryProvider serviceEntryProvider) + { + _serviceEntryProvider = serviceEntryProvider; + } + + public IEnumerable GetSchemaFilesPath() + { + var result = new List(); + var assemblieFiles = _serviceEntryProvider.GetALLEntries() + .Select(p => p.Type.Assembly.Location).Distinct(); + + foreach (var assemblieFile in assemblieFiles) + { + var fileSpan = assemblieFile.AsSpan(); + var path = $"{fileSpan.Slice(0, fileSpan.LastIndexOf(".")).ToString()}.xml"; + if (File.Exists(path)) + result.Add(path); + } + return result; + } + } +} diff --git a/src/Surging.Core/Surging.Core.Swagger_V5/Internal/IServiceSchemaProvider.cs b/src/Surging.Core/Surging.Core.Swagger_V5/Internal/IServiceSchemaProvider.cs new file mode 100644 index 000000000..dbbf23d21 --- /dev/null +++ b/src/Surging.Core/Surging.Core.Swagger_V5/Internal/IServiceSchemaProvider.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Surging.Core.Swagger_V5.Internal +{ + public interface IServiceSchemaProvider + { + IEnumerable GetSchemaFilesPath(); + } +} diff --git a/src/Surging.Core/Surging.Core.Swagger_V5/Surging.Core.Swagger_V5.csproj b/src/Surging.Core/Surging.Core.Swagger_V5/Surging.Core.Swagger_V5.csproj new file mode 100644 index 000000000..da24da1a5 --- /dev/null +++ b/src/Surging.Core/Surging.Core.Swagger_V5/Surging.Core.Swagger_V5.csproj @@ -0,0 +1,67 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Surging.Core/Surging.Core.Swagger_V5/Swagger/DependencyInjection/SwaggerBuilderExtensions.cs b/src/Surging.Core/Surging.Core.Swagger_V5/Swagger/DependencyInjection/SwaggerBuilderExtensions.cs new file mode 100644 index 000000000..e057a7656 --- /dev/null +++ b/src/Surging.Core/Surging.Core.Swagger_V5/Swagger/DependencyInjection/SwaggerBuilderExtensions.cs @@ -0,0 +1,73 @@ +using System; +using System.Linq; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing; + +#if (!NETSTANDARD2_0) +using Microsoft.AspNetCore.Routing.Patterns; +#endif + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Surging.Core.Swagger_V5.Swagger; + +namespace Surging.Core.Swagger_V5.Builder +{ + public static class SwaggerBuilderExtensions + { + /// + /// Register the Swagger middleware with provided options + /// + public static IApplicationBuilder UseSwagger(this IApplicationBuilder app, SwaggerOptions options) + { + return app.UseMiddleware(options); + } + + /// + /// Register the Swagger middleware with optional setup action for DI-injected options + /// + public static IApplicationBuilder UseSwagger( + this IApplicationBuilder app, + Action setupAction = null) + { + SwaggerOptions options; + using (var scope = app.ApplicationServices.CreateScope()) + { + options = scope.ServiceProvider.GetRequiredService>().Value; + setupAction?.Invoke(options); + } + + return app.UseSwagger(options); + } + +#if (!NETSTANDARD2_0) + public static IEndpointConventionBuilder MapSwagger( + this IEndpointRouteBuilder endpoints, + string pattern = "/swagger/{documentName}/swagger.{json|yaml}", + Action setupAction = null) + { + if (!RoutePatternFactory.Parse(pattern).Parameters.Any(x => x.Name == "documentName")) + { + throw new ArgumentException("Pattern must contain '{documentName}' parameter", nameof(pattern)); + } + + Action endpointSetupAction = options => + { + var endpointOptions = new SwaggerEndpointOptions(); + + setupAction?.Invoke(endpointOptions); + + options.RouteTemplate = pattern; + options.SerializeAsV2 = endpointOptions.SerializeAsV2; + options.PreSerializeFilters.AddRange(endpointOptions.PreSerializeFilters); + }; + + var pipeline = endpoints.CreateApplicationBuilder() + .UseSwagger(endpointSetupAction) + .Build(); + + return endpoints.MapGet(pattern, pipeline); + } +#endif + } +} \ No newline at end of file diff --git a/src/Surging.Core/Surging.Core.Swagger_V5/Swagger/Filters/AddAuthorizationOperationFilter.cs b/src/Surging.Core/Surging.Core.Swagger_V5/Swagger/Filters/AddAuthorizationOperationFilter.cs new file mode 100644 index 000000000..7ab5c78c5 --- /dev/null +++ b/src/Surging.Core/Surging.Core.Swagger_V5/Swagger/Filters/AddAuthorizationOperationFilter.cs @@ -0,0 +1,70 @@ + +using Microsoft.OpenApi.Models; +using Surging.Core.CPlatform.Filters.Implementation; +using Surging.Core.Swagger_V5.Swagger.Model; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Surging.Core.Swagger_V5.SwaggerGen.Filters +{ + public class AddAuthorizationOperationFilter : IOperationFilter + { + + public AddAuthorizationOperationFilter() + { + } + + + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + if (operation.Parameters == null) + { + operation.Parameters = new List(); + } + + + var attribute = + context.ServiceEntry.Attributes.Where(p => p is AuthorizationAttribute) + .Select(p => p as AuthorizationAttribute).FirstOrDefault(); + if (attribute != null && attribute.AuthType == AuthorizationType.JWT) + { + operation.Parameters.Add(new OpenApiParameter + { + Name = "Authorization", + In = ParameterLocation.Header, + Required = false, + Schema = new OpenApiSchema { + + Type= "string" + } + }); + } + else if (attribute != null && attribute.AuthType == AuthorizationType.AppSecret) + { + operation.Parameters.Add(new OpenApiParameter + { + Name = "Authorization", + In = ParameterLocation.Header, + Required = false, + Schema = new OpenApiSchema + { + Type = "string" + } + }); + operation.Parameters.Add(new OpenApiParameter + { + Name = "timeStamp", + In = ParameterLocation.Query, + Required = false, + Schema = new OpenApiSchema + { + Type = "string" + } + }); + } + } + } +} diff --git a/src/Surging.Core/Surging.Core.Swagger_V5/Swagger/ISwaggerProvider.cs b/src/Surging.Core/Surging.Core.Swagger_V5/Swagger/ISwaggerProvider.cs new file mode 100644 index 000000000..f708147c1 --- /dev/null +++ b/src/Surging.Core/Surging.Core.Swagger_V5/Swagger/ISwaggerProvider.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Models; + +namespace Surging.Core.Swagger_V5.Swagger +{ + public interface ISwaggerProvider + { + OpenApiDocument GetSwagger( + string documentName, + string host = null, + string basePath = null); + } + + public class UnknownSwaggerDocument : InvalidOperationException + { + public UnknownSwaggerDocument(string documentName, IEnumerable knownDocuments) + : base(string.Format("Unknown Swagger document - \"{0}\". Known Swagger documents: {1}", + documentName, + string.Join(",", knownDocuments?.Select(x => $"\"{x}\"")))) + {} + } +} \ No newline at end of file diff --git a/src/Surging.Core/Surging.Core.Swagger_V5/Swagger/Model/SecurityScheme.cs b/src/Surging.Core/Surging.Core.Swagger_V5/Swagger/Model/SecurityScheme.cs new file mode 100644 index 000000000..4a464943d --- /dev/null +++ b/src/Surging.Core/Surging.Core.Swagger_V5/Swagger/Model/SecurityScheme.cs @@ -0,0 +1,20 @@ +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace Surging.Core.Swagger_V5.Swagger.Model +{ + public abstract class SecurityScheme + { + public SecurityScheme() + { + Extensions = new Dictionary(); + } + + public string Type { get; set; } + + public string Description { get; set; } + + [JsonExtensionData] + public Dictionary Extensions { get; private set; } + } +} diff --git a/src/Surging.Core/Surging.Core.Swagger_V5/Swagger/Model/SwaggerDocument.cs b/src/Surging.Core/Surging.Core.Swagger_V5/Swagger/Model/SwaggerDocument.cs new file mode 100644 index 000000000..dedbce86e --- /dev/null +++ b/src/Surging.Core/Surging.Core.Swagger_V5/Swagger/Model/SwaggerDocument.cs @@ -0,0 +1,401 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Surging.Core.Swagger_V5.Swagger.Model +{ + public class SwaggerDocument + { + public SwaggerDocument() + { + Extensions = new Dictionary(); + } + + public string Swagger + { + get { return "5.0"; } + } + + public Info Info { get; set; } + + public string Host { get; set; } + + public string BasePath { get; set; } + + public IList Schemes { get; set; } + + public IList Consumes { get; set; } + + public IList Produces { get; set; } + + public IDictionary Paths { get; set; } + + public IDictionary Definitions { get; set; } + + public IDictionary Parameters { get; set; } + + public IDictionary Responses { get; set; } + + public IDictionary SecurityDefinitions { get; set; } + + public IList>> Security { get; set; } + + public IList Tags { get; set; } + + public ExternalDocs ExternalDocs { get; set; } + + [JsonExtensionData] + public Dictionary Extensions { get; private set; } + } + + public class DocumentConfiguration + { + public Info Info { get; set; } = null; + + public DocumentOptions Options { get; set; } = null; + } + + public class DocumentOptions + { + public bool IgnoreFullyQualified { get; set; } + + public string IngressName { get; set; } + + public IEnumerable MapRoutePaths { get; set; } + } + + public class MapRoutePath + { + public string SourceRoutePath { get; set; } + + public string TargetRoutePath { get; set; } + } + + public class Info + { + public Info() + { + Extensions = new Dictionary(); + } + + public string Version { get; set; } + + public string Title { get; set; } + + public string Description { get; set; } + + public string TermsOfService { get; set; } + + public Contact Contact { get; set; } + + public License License { get; set; } + + [JsonExtensionData] + public Dictionary Extensions { get; private set; } + } + + public class Contact + { + public string Name { get; set; } + + public string Url { get; set; } + + public string Email { get; set; } + } + + public class License + { + public string Name { get; set; } + + public string Url { get; set; } + } + + public class PathItem + { + public PathItem() + { + Extensions = new Dictionary(); + } + + [JsonProperty("$ref")] + public string Ref { get; set; } + + public Operation Get { get; set; } + + public Operation Put { get; set; } + + public Operation Post { get; set; } + + public Operation Delete { get; set; } + + public Operation Options { get; set; } + + public Operation Head { get; set; } + + public Operation Patch { get; set; } + + public IList Parameters { get; set; } + + [JsonExtensionData] + public Dictionary Extensions { get; private set; } + } + + public class Operation + { + public Operation() + { + Extensions = new Dictionary(); + } + + public IList Tags { get; set; } + + public string Summary { get; set; } + + public string Description { get; set; } + + public ExternalDocs ExternalDocs { get; set; } + + public string OperationId { get; set; } + + public IList Consumes { get; set; } + + public IList Produces { get; set; } + + public IList Parameters { get; set; } + + public IDictionary Responses { get; set; } + + public IList Schemes { get; set; } + + public bool? Deprecated { get; set; } + + public IList>> Security { get; set; } + + [JsonExtensionData] + public Dictionary Extensions { get; private set; } + } + + public class Tag + { + public Tag() + { + Extensions = new Dictionary(); + } + + public string Name { get; set; } + + public string Description { get; set; } + + public ExternalDocs ExternalDocs { get; set; } + + [JsonExtensionData] + public Dictionary Extensions { get; private set; } + } + + public class ExternalDocs + { + public string Description { get; set; } + + public string Url { get; set; } + } + + + public interface IParameter + { + string Name { get; set; } + + string In { get; set; } + + string Description { get; set; } + + bool Required { get; set; } + + Dictionary Extensions { get; } + } + + public class BodyParameter : IParameter + { + public BodyParameter() + { + In = "body"; + Extensions = new Dictionary(); + } + + public string Name { get; set; } + + public string In { get; set; } + + public string Description { get; set; } + + public bool Required { get; set; } + + [JsonExtensionData] + public Dictionary Extensions { get; private set; } + + public Schema Schema { get; set; } + } + + public class NonBodyParameter : PartialSchema, IParameter + { + public string Name { get; set; } + + public string In { get; set; } + + public string Description { get; set; } + + public bool Required { get; set; } + } + + public class Schema + { + public Schema() + { + Extensions = new Dictionary(); + } + + [JsonProperty("$ref")] + public string Ref { get; set; } + + public string Format { get; set; } + + public string Title { get; set; } + + public string Description { get; set; } + + public object Default { get; set; } + + public int? MultipleOf { get; set; } + + public double? Maximum { get; set; } + + public bool? ExclusiveMaximum { get; set; } + + public double? Minimum { get; set; } + + public bool? ExclusiveMinimum { get; set; } + + public int? MaxLength { get; set; } + + public int? MinLength { get; set; } + + public string Pattern { get; set; } + + public int? MaxItems { get; set; } + + public int? MinItems { get; set; } + + public bool? UniqueItems { get; set; } + + public int? MaxProperties { get; set; } + + public int? MinProperties { get; set; } + + public IList Required { get; set; } + + public IList Enum { get; set; } + + public string Type { get; set; } + + public Schema Items { get; set; } + + public IList AllOf { get; set; } + + public IDictionary Properties { get; set; } + + public Schema AdditionalProperties { get; set; } + + public string Discriminator { get; set; } + + public bool? ReadOnly { get; set; } + + public Xml Xml { get; set; } + + public ExternalDocs ExternalDocs { get; set; } + + public object Example { get; set; } + + [JsonExtensionData] + public Dictionary Extensions { get; private set; } + } + + public class PartialSchema + { + public PartialSchema() + { + Extensions = new Dictionary(); + } + + public string Type { get; set; } + + public string Format { get; set; } + + public PartialSchema Items { get; set; } + + public string CollectionFormat { get; set; } + + public object Default { get; set; } + + public double? Maximum { get; set; } + + public bool? ExclusiveMaximum { get; set; } + + public double? Minimum { get; set; } + + public bool? ExclusiveMinimum { get; set; } + + public int? MaxLength { get; set; } + + public int? MinLength { get; set; } + + public string Pattern { get; set; } + + public int? MaxItems { get; set; } + + public int? MinItems { get; set; } + + public bool? UniqueItems { get; set; } + + public IList Enum { get; set; } + + public int? MultipleOf { get; set; } + + [JsonExtensionData] + public Dictionary Extensions { get; private set; } + } + + public class Response + { + public Response() + { + Extensions = new Dictionary(); + } + + public string Description { get; set; } + + public Schema Schema { get; set; } + + public IDictionary Headers { get; set; } + + public object Examples { get; set; } + + [JsonExtensionData] + public Dictionary Extensions { get; private set; } + } + + public class Header : PartialSchema + { + public string Description { get; set; } + } + + public class Xml + { + public string Name { get; set; } + + public string Namespace { get; set; } + + public string Prefix { get; set; } + + public bool? Attribute { get; set; } + + public bool? Wrapped { get; set; } + } +} diff --git a/src/Surging.Core/Surging.Core.Swagger_V5/Swagger/SwaggerEndpointOptions.cs b/src/Surging.Core/Surging.Core.Swagger_V5/Swagger/SwaggerEndpointOptions.cs new file mode 100644 index 000000000..d51b708a8 --- /dev/null +++ b/src/Surging.Core/Surging.Core.Swagger_V5/Swagger/SwaggerEndpointOptions.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.AspNetCore.Http; +using Microsoft.OpenApi.Models; + +namespace Surging.Core.Swagger_V5.Swagger +{ + public class SwaggerEndpointOptions + { + public SwaggerEndpointOptions() + { + PreSerializeFilters = new List>(); + SerializeAsV2 = false; + } + + + /// + /// Return Swagger JSON in the V2 format rather than V3 + /// + public bool SerializeAsV2 { get; set; } + + /// + /// Actions that can be applied SwaggerDocument's before they're serialized to JSON. + /// Useful for setting metadata that's derived from the current request + /// + public List> PreSerializeFilters { get; private set; } + } +} diff --git a/src/Surging.Core/Surging.Core.Swagger_V5/Swagger/SwaggerMiddleware.cs b/src/Surging.Core/Surging.Core.Swagger_V5/Swagger/SwaggerMiddleware.cs new file mode 100644 index 000000000..ef39bdaf9 --- /dev/null +++ b/src/Surging.Core/Surging.Core.Swagger_V5/Swagger/SwaggerMiddleware.cs @@ -0,0 +1,113 @@ +using System.Globalization; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Template; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Writers; + +namespace Surging.Core.Swagger_V5.Swagger +{ + public class SwaggerMiddleware + { + private readonly RequestDelegate _next; + private readonly SwaggerOptions _options; + private readonly TemplateMatcher _requestMatcher; + + public SwaggerMiddleware( + RequestDelegate next, + SwaggerOptions options) + { + _next = next; + _options = options ?? new SwaggerOptions(); + _requestMatcher = new TemplateMatcher(TemplateParser.Parse(_options.RouteTemplate), new RouteValueDictionary()); + } + + public async Task Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider) + { + if (!RequestingSwaggerDocument(httpContext.Request, out string documentName)) + { + await _next(httpContext); + return; + } + + try + { + var basePath = httpContext.Request.PathBase.HasValue + ? httpContext.Request.PathBase.Value + : null; + + var swagger = swaggerProvider.GetSwagger( + documentName: documentName, + host: null, + basePath: basePath); + + // One last opportunity to modify the Swagger Document - this time with request context + foreach (var filter in _options.PreSerializeFilters) + { + filter(swagger, httpContext.Request); + } + + if (Path.GetExtension(httpContext.Request.Path.Value) == ".yaml") + { + await RespondWithSwaggerYaml(httpContext.Response, swagger); + } + else + { + await RespondWithSwaggerJson(httpContext.Response, swagger); + } + } + catch (UnknownSwaggerDocument) + { + RespondWithNotFound(httpContext.Response); + } + } + + private bool RequestingSwaggerDocument(HttpRequest request, out string documentName) + { + documentName = null; + if (request.Method != "GET") return false; + + var routeValues = new RouteValueDictionary(); + if (!_requestMatcher.TryMatch(request.Path, routeValues) || !routeValues.ContainsKey("documentName")) return false; + + documentName = routeValues["documentName"].ToString(); + return true; + } + + private void RespondWithNotFound(HttpResponse response) + { + response.StatusCode = 404; + } + + private async Task RespondWithSwaggerJson(HttpResponse response, OpenApiDocument swagger) + { + response.StatusCode = 200; + response.ContentType = "application/json;charset=utf-8"; + + using (var textWriter = new StringWriter(CultureInfo.InvariantCulture)) + { + var jsonWriter = new OpenApiJsonWriter(textWriter); + if (_options.SerializeAsV2) swagger.SerializeAsV2(jsonWriter); else swagger.SerializeAsV3(jsonWriter); + + await response.WriteAsync(textWriter.ToString(), new UTF8Encoding(false)); + } + } + + private async Task RespondWithSwaggerYaml(HttpResponse response, OpenApiDocument swagger) + { + response.StatusCode = 200; + response.ContentType = "text/yaml;charset=utf-8"; + + using (var textWriter = new StringWriter(CultureInfo.InvariantCulture)) + { + var yamlWriter = new OpenApiYamlWriter(textWriter); + if (_options.SerializeAsV2) swagger.SerializeAsV2(yamlWriter); else swagger.SerializeAsV3(yamlWriter); + + await response.WriteAsync(textWriter.ToString(), new UTF8Encoding(false)); + } + } + } +} diff --git a/src/Surging.Core/Surging.Core.Swagger_V5/Swagger/SwaggerOptions.cs b/src/Surging.Core/Surging.Core.Swagger_V5/Swagger/SwaggerOptions.cs new file mode 100644 index 000000000..bd0b06388 --- /dev/null +++ b/src/Surging.Core/Surging.Core.Swagger_V5/Swagger/SwaggerOptions.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Http; +using Microsoft.OpenApi.Models; + +namespace Surging.Core.Swagger_V5.Swagger +{ + public class SwaggerOptions + { + public SwaggerOptions() + { + PreSerializeFilters = new List>(); + SerializeAsV2 = false; + } + + /// + /// Sets a custom route for the Swagger JSON/YAML endpoint(s). Must include the {documentName} parameter + /// + public string RouteTemplate { get; set; } = "swagger/{documentName}/swagger.{json|yaml}"; + + /// + /// Return Swagger JSON/YAML in the V2 format rather than V3 + /// + public bool SerializeAsV2 { get; set; } + + /// + /// Actions that can be applied to an OpenApiDocument before it's serialized. + /// Useful for setting metadata that's derived from the current request + /// + public List> PreSerializeFilters { get; private set; } + } +} diff --git a/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/DependencyInjection/ConfigureSchemaGeneratorOptions.cs b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/DependencyInjection/ConfigureSchemaGeneratorOptions.cs new file mode 100644 index 000000000..a73eb1a92 --- /dev/null +++ b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/DependencyInjection/ConfigureSchemaGeneratorOptions.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.OpenApi.Models; + +namespace Surging.Core.Swagger_V5.SwaggerGen +{ + internal class ConfigureSchemaGeneratorOptions : IConfigureOptions + { + private readonly SwaggerGenOptions _swaggerGenOptions; + private readonly IServiceProvider _serviceProvider; + + public ConfigureSchemaGeneratorOptions( + IOptions swaggerGenOptionsAccessor, + IServiceProvider serviceProvider) + { + _swaggerGenOptions = swaggerGenOptionsAccessor.Value; + _serviceProvider = serviceProvider; + } + + public void Configure(SchemaGeneratorOptions options) + { + DeepCopy(_swaggerGenOptions.SchemaGeneratorOptions, options); + + // Create and add any filters that were specified through the FilterDescriptor lists + _swaggerGenOptions.SchemaFilterDescriptors.ForEach( + filterDescriptor => options.SchemaFilters.Add(CreateFilter(filterDescriptor))); + } + + private void DeepCopy(SchemaGeneratorOptions source, SchemaGeneratorOptions target) + { + target.CustomTypeMappings = new Dictionary>(source.CustomTypeMappings); + target.UseInlineDefinitionsForEnums = source.UseInlineDefinitionsForEnums; + target.IgnoreFullyQualified = source.IgnoreFullyQualified; + target.SchemaIdSelector = source.SchemaIdSelector; + target.IgnoreObsoleteProperties = source.IgnoreObsoleteProperties; + target.UseAllOfForInheritance = source.UseAllOfForInheritance; + target.UseOneOfForPolymorphism = source.UseOneOfForPolymorphism; + target.SubTypesSelector = source.SubTypesSelector; + target.DiscriminatorNameSelector = source.DiscriminatorNameSelector; + target.DiscriminatorValueSelector = source.DiscriminatorValueSelector; + target.UseAllOfToExtendReferenceSchemas = source.UseAllOfToExtendReferenceSchemas; + target.SupportNonNullableReferenceTypes = source.SupportNonNullableReferenceTypes; + target.SchemaFilters = new List(source.SchemaFilters); + } + + private TFilter CreateFilter(FilterDescriptor filterDescriptor) + { + return (TFilter)ActivatorUtilities + .CreateInstance(_serviceProvider, filterDescriptor.Type, filterDescriptor.Arguments); + } + } +} \ No newline at end of file diff --git a/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/DependencyInjection/ConfigureSwaggerGeneratorOptions.cs b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/DependencyInjection/ConfigureSwaggerGeneratorOptions.cs new file mode 100644 index 000000000..05c5fe447 --- /dev/null +++ b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/DependencyInjection/ConfigureSwaggerGeneratorOptions.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.OpenApi.Models; + +#if NETSTANDARD2_0 +using IWebHostEnvironment = Microsoft.AspNetCore.Hosting.IHostingEnvironment; +#endif + +namespace Surging.Core.Swagger_V5.SwaggerGen +{ + internal class ConfigureSwaggerGeneratorOptions : IConfigureOptions + { + private readonly SwaggerGenOptions _swaggerGenOptions; + private readonly IServiceProvider _serviceProvider; + private readonly IWebHostEnvironment _hostingEnv; + + public ConfigureSwaggerGeneratorOptions( + IOptions swaggerGenOptionsAccessor, + IServiceProvider serviceProvider, + IWebHostEnvironment hostingEnv) + { + _swaggerGenOptions = swaggerGenOptionsAccessor.Value; + _serviceProvider = serviceProvider; + _hostingEnv = hostingEnv; + } + + public void Configure(SwaggerGeneratorOptions options) + { + DeepCopy(_swaggerGenOptions.SwaggerGeneratorOptions, options); + + // Create and add any filters that were specified through the FilterDescriptor lists ... + + _swaggerGenOptions.ParameterFilterDescriptors.ForEach( + filterDescriptor => options.ParameterFilters.Add(CreateFilter(filterDescriptor))); + + _swaggerGenOptions.RequestBodyFilterDescriptors.ForEach( + filterDescriptor => options.RequestBodyFilters.Add(CreateFilter(filterDescriptor))); + + _swaggerGenOptions.OperationFilterDescriptors.ForEach( + filterDescriptor => options.OperationFilters.Add(CreateFilter(filterDescriptor))); + + _swaggerGenOptions.DocumentFilterDescriptors.ForEach( + filterDescriptor => options.DocumentFilters.Add(CreateFilter(filterDescriptor))); + + if (!options.SwaggerDocs.Any()) + { + options.SwaggerDocs.Add("v1", new OpenApiInfo { Title = _hostingEnv.ApplicationName, Version = "1.0" }); + } + } + + public void DeepCopy(SwaggerGeneratorOptions source, SwaggerGeneratorOptions target) + { + target.SwaggerDocs = new Dictionary(source.SwaggerDocs); + target.DocInclusionPredicate = source.DocInclusionPredicate; + target.DocInclusionPredicateV2 = source.DocInclusionPredicateV2; + target.IgnoreObsoleteActions = source.IgnoreObsoleteActions; + target.ConflictingActionsResolver = source.ConflictingActionsResolver; + target.OperationIdSelector = source.OperationIdSelector; + target.EntryOperationIdSelector = source.EntryOperationIdSelector; + target.TagsSelector = source.TagsSelector; + target.EntryTagsSelector = source.EntryTagsSelector; + target.SortKeySelector = source.SortKeySelector; + target.DescribeAllParametersInCamelCase = source.DescribeAllParametersInCamelCase; + target.Servers = new List(source.Servers); + target.SecuritySchemes = new Dictionary(source.SecuritySchemes); + target.SecurityRequirements = new List(source.SecurityRequirements); + target.ParameterFilters = new List(source.ParameterFilters); + target.OperationFilters = new List(source.OperationFilters); + target.DocumentFilters = new List(source.DocumentFilters); + } + + private TFilter CreateFilter(FilterDescriptor filterDescriptor) + { + return (TFilter)ActivatorUtilities + .CreateInstance(_serviceProvider, filterDescriptor.Type, filterDescriptor.Arguments); + } + } +} \ No newline at end of file diff --git a/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/DependencyInjection/DocumentProvider.cs b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/DependencyInjection/DocumentProvider.cs new file mode 100644 index 000000000..d888314a8 --- /dev/null +++ b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/DependencyInjection/DocumentProvider.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Microsoft.OpenApi.Writers; +using Surging.Core.Swagger_V5.Swagger; +using Surging.Core.Swagger_V5.SwaggerGen; + +namespace Surging.Core.Swagger_V5.ApiDescriptions +{ + /// + /// This service will be looked up by name from the service collection when using + /// the dotnet-getdocument tool from the Microsoft.Extensions.ApiDescription.Server package. + /// + internal interface IDocumentProvider + { + IEnumerable GetDocumentNames(); + + Task GenerateAsync(string documentName, TextWriter writer); + } + + internal class DocumentProvider : IDocumentProvider + { + private readonly SwaggerGeneratorOptions _generatorOptions; + private readonly SwaggerOptions _options; + private readonly ISwaggerProvider _swaggerProvider; + + public DocumentProvider( + IOptions generatorOptions, + IOptions options, + ISwaggerProvider swaggerProvider) + { + _generatorOptions = generatorOptions.Value; + _options = options.Value; + _swaggerProvider = swaggerProvider; + } + + public IEnumerable GetDocumentNames() + { + return _generatorOptions.SwaggerDocs.Keys; + } + + public Task GenerateAsync(string documentName, TextWriter writer) + { + // Let UnknownSwaggerDocument or other exception bubble up to caller. + var swagger = _swaggerProvider.GetSwagger(documentName, host: null, basePath: null); + var jsonWriter = new OpenApiJsonWriter(writer); + if (_options.SerializeAsV2) + { + swagger.SerializeAsV2(jsonWriter); + } + else + { + swagger.SerializeAsV3(jsonWriter); + } + + return Task.CompletedTask; + } + } +} diff --git a/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/DependencyInjection/SwaggerApplicationConvention.cs b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/DependencyInjection/SwaggerApplicationConvention.cs new file mode 100644 index 000000000..46ab9a6d0 --- /dev/null +++ b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/DependencyInjection/SwaggerApplicationConvention.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Mvc.ApplicationModels; + +namespace Surging.Core.Swagger_V5.SwaggerGen +{ + public class SwaggerApplicationConvention : IApplicationModelConvention + { + public void Apply(ApplicationModel application) + { + application.ApiExplorer.IsVisible = true; + } + } +} \ No newline at end of file diff --git a/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/DependencyInjection/SwaggerGenOptions.cs b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/DependencyInjection/SwaggerGenOptions.cs new file mode 100644 index 000000000..fcbb14d5d --- /dev/null +++ b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/DependencyInjection/SwaggerGenOptions.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; + +namespace Surging.Core.Swagger_V5.SwaggerGen +{ + public class SwaggerGenOptions + { + public SwaggerGeneratorOptions SwaggerGeneratorOptions { get; set; } = new SwaggerGeneratorOptions(); + + public SchemaGeneratorOptions SchemaGeneratorOptions { get; set; } = new SchemaGeneratorOptions(); + + // NOTE: Filter instances can be added directly to the options exposed above OR they can be specified in + // the following lists. In the latter case, they will be instantiated and added when options are injected + // into their target services. This "deferred instantiation" allows the filters to be created from the + // DI container, thus supporting contructor injection of services within filters. + + public List ParameterFilterDescriptors { get; set; } = new List(); + + public List RequestBodyFilterDescriptors { get; set; } = new List(); + + public List OperationFilterDescriptors { get; set; } = new List(); + + public List DocumentFilterDescriptors { get; set; } = new List(); + + public List SchemaFilterDescriptors { get; set; } = new List(); + } + + public class FilterDescriptor + { + public Type Type { get; set; } + + public object[] Arguments { get; set; } + } +} \ No newline at end of file diff --git a/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/DependencyInjection/SwaggerGenOptionsExtensions.cs b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/DependencyInjection/SwaggerGenOptionsExtensions.cs new file mode 100644 index 000000000..1d93326ca --- /dev/null +++ b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/DependencyInjection/SwaggerGenOptionsExtensions.cs @@ -0,0 +1,537 @@ +using System; +using System.Collections.Generic; +using System.Xml.XPath; +using Microsoft.OpenApi.Models; +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Surging.Core.Swagger_V5.SwaggerGen; +using Surging.Core.CPlatform.Runtime.Server; +using System.Reflection; +using Surging.Core.Swagger_V5.Swagger.Model; + +namespace Surging.Core.Swagger_V5.DependencyInjection +{ + public static class SwaggerGenOptionsExtensions + { + /// + /// Define one or more documents to be created by the Swagger generator + /// + /// + /// A URI-friendly name that uniquely identifies the document + /// Global metadata to be included in the Swagger output + public static void SwaggerDoc( + this SwaggerGenOptions swaggerGenOptions, + string name, + OpenApiInfo info) + { + swaggerGenOptions.SwaggerGeneratorOptions.SwaggerDocs.Add(name, info); + } + + /// + /// Provide a custom strategy for selecting actions. + /// + /// + /// + /// A lambda that returns true/false based on document name and ApiDescription + /// + public static void DocInclusionPredicate( + this SwaggerGenOptions swaggerGenOptions, + Func predicate) + { + swaggerGenOptions.SwaggerGeneratorOptions.DocInclusionPredicate = predicate; + } + + /// + /// Ignore any actions that are decorated with the ObsoleteAttribute + /// + public static void IgnoreObsoleteActions(this SwaggerGenOptions swaggerGenOptions) + { + swaggerGenOptions.SwaggerGeneratorOptions.IgnoreObsoleteActions = true; + } + + /// + /// Merge actions that have conflicting HTTP methods and paths (must be unique for Swagger 2.0) + /// + /// + /// + public static void ResolveConflictingActions( + this SwaggerGenOptions swaggerGenOptions, + Func, ApiDescription> resolver) + { + swaggerGenOptions.SwaggerGeneratorOptions.ConflictingActionsResolver = resolver; + } + + /// + /// Provide a custom strategy for assigning "operationId" to operations + /// + public static void CustomOperationIds( + this SwaggerGenOptions swaggerGenOptions, + Func operationIdSelector) + { + swaggerGenOptions.SwaggerGeneratorOptions.OperationIdSelector = operationIdSelector; + } + + public static void CustomEntryOperationIds( + this SwaggerGenOptions swaggerGenOptions, + Func operationIdSelector) + { + swaggerGenOptions.SwaggerGeneratorOptions.EntryOperationIdSelector = operationIdSelector; + } + + + /// + /// Provide a custom strategy for assigning a default "tag" to operations + /// + /// + /// + [Obsolete("Deprecated: Use the overload that accepts a Func that returns a list of tags")] + public static void TagActionsBy( + this SwaggerGenOptions swaggerGenOptions, + Func tagSelector) + { + swaggerGenOptions.SwaggerGeneratorOptions.TagsSelector = (apiDesc) => new[] { tagSelector(apiDesc) }; + } + + public static void EntryTagActionsBy( + this SwaggerGenOptions swaggerGenOptions, + Func tagSelector) + { + swaggerGenOptions.SwaggerGeneratorOptions.EntryTagsSelector = (apiDesc) => new[] { tagSelector(apiDesc) }; + } + + public static void EntryTagActionsBy( +this SwaggerGenOptions swaggerGenOptions, +Func> tagSelector) + { + swaggerGenOptions.SwaggerGeneratorOptions.EntryTagsSelector = tagSelector; + } + + /// + /// Provide a custom strategy for assigning "tags" to actions + /// + /// + /// + public static void TagActionsBy( + this SwaggerGenOptions swaggerGenOptions, + Func> tagsSelector) + { + swaggerGenOptions.SwaggerGeneratorOptions.TagsSelector = tagsSelector; + } + + public static void DocInclusionPredicateV2( +this SwaggerGenOptions swaggerGenOptions, +Func predicate) + { + swaggerGenOptions.SwaggerGeneratorOptions.DocInclusionPredicateV2 = predicate; + } + + /// + /// Provide a custom strategy for sorting actions before they're transformed into the Swagger format + /// + /// + /// + public static void OrderActionsBy( + this SwaggerGenOptions swaggerGenOptions, + Func sortKeySelector) + { + swaggerGenOptions.SwaggerGeneratorOptions.SortKeySelector = sortKeySelector; + } + + /// + /// Provide a custom comprarer to sort schemas with + /// + /// + /// + public static void SortSchemasWith( + this SwaggerGenOptions swaggerGenOptions, + IComparer schemaComparer) + { + swaggerGenOptions.SwaggerGeneratorOptions.SchemaComparer = schemaComparer; + } + + /// + /// Describe all parameters, regardless of how they appear in code, in camelCase + /// + public static void DescribeAllParametersInCamelCase(this SwaggerGenOptions swaggerGenOptions) + { + swaggerGenOptions.SwaggerGeneratorOptions.DescribeAllParametersInCamelCase = true; + } + + + /// + /// Provide specific server information to include in the generated Swagger document + /// + /// + /// A description of the server + public static void AddServer(this SwaggerGenOptions swaggerGenOptions, OpenApiServer server) + { + swaggerGenOptions.SwaggerGeneratorOptions.Servers.Add(server); + } + + /// + /// Add one or more "securityDefinitions", describing how your API is protected, to the generated Swagger + /// + /// + /// A unique name for the scheme, as per the Swagger spec. + /// + /// A description of the scheme - can be an instance of BasicAuthScheme, ApiKeyScheme or OAuth2Scheme + /// + public static void AddSecurityDefinition( + this SwaggerGenOptions swaggerGenOptions, + string name, + OpenApiSecurityScheme securityScheme) + { + swaggerGenOptions.SwaggerGeneratorOptions.SecuritySchemes.Add(name, securityScheme); + } + + /// + /// Adds a global security requirement + /// + /// + /// + /// A dictionary of required schemes (logical AND). Keys must correspond to schemes defined through AddSecurityDefinition + /// If the scheme is of type "oauth2", then the value is a list of scopes, otherwise it MUST be an empty array + /// + public static void AddSecurityRequirement( + this SwaggerGenOptions swaggerGenOptions, + OpenApiSecurityRequirement securityRequirement) + { + swaggerGenOptions.SwaggerGeneratorOptions.SecurityRequirements.Add(securityRequirement); + } + + public static void IgnoreFullyQualified(this SwaggerGenOptions swaggerGenOptions) + { + swaggerGenOptions.SchemaGeneratorOptions.IgnoreFullyQualified = true; + } + + /// + /// Provide a custom mapping, for a given type, to the Swagger-flavored JSONSchema + /// + /// + /// System type + /// A factory method that generates Schema's for the provided type + public static void MapType( + this SwaggerGenOptions swaggerGenOptions, + Type type, + Func schemaFactory) + { + swaggerGenOptions.SchemaGeneratorOptions.CustomTypeMappings.Add(type, schemaFactory); + } + + /// + /// Provide a custom mapping, for a given type, to the Swagger-flavored JSONSchema + /// + /// System type + /// + /// A factory method that generates Schema's for the provided type + public static void MapType( + this SwaggerGenOptions swaggerGenOptions, + Func schemaFactory) + { + swaggerGenOptions.MapType(typeof(T), schemaFactory); + } + + /// + /// Generate inline schema definitions (as opposed to referencing a shared definition) for enum parameters and properties + /// + /// + public static void UseInlineDefinitionsForEnums(this SwaggerGenOptions swaggerGenOptions) + { + swaggerGenOptions.SchemaGeneratorOptions.UseInlineDefinitionsForEnums = true; + } + + /// + /// Provide a custom strategy for generating the unique Id's that are used to reference object Schema's + /// + /// + /// + /// A lambda that returns a unique identifier for the provided system type + /// + public static void CustomSchemaIds( + this SwaggerGenOptions swaggerGenOptions, + Func schemaIdSelector) + { + swaggerGenOptions.SchemaGeneratorOptions.SchemaIdSelector = schemaIdSelector; + } + + /// + /// Ignore any properties that are decorated with the ObsoleteAttribute + /// + public static void IgnoreObsoleteProperties(this SwaggerGenOptions swaggerGenOptions) + { + swaggerGenOptions.SchemaGeneratorOptions.IgnoreObsoleteProperties = true; + } + + /// + /// Enables composite schema generation. If enabled, subtype schemas will contain the allOf construct to + /// incorporate properties from the base class instead of defining those properties inline. + /// + /// + public static void UseAllOfForInheritance(this SwaggerGenOptions swaggerGenOptions) + { + swaggerGenOptions.SchemaGeneratorOptions.UseAllOfForInheritance = true; + } + + /// + /// Enables polymorphic schema generation. If enabled, request and response schemas will contain the oneOf + /// construct to describe sub types as a set of alternative schemas. + /// + /// + public static void UseOneOfForPolymorphism(this SwaggerGenOptions swaggerGenOptions) + { + swaggerGenOptions.SchemaGeneratorOptions.UseOneOfForPolymorphism = true; + } + + /// + /// To support polymorphism and inheritance behavior, Swashbuckle needs to detect the "known" subtypes for a given base type. + /// That is, the subtypes exposed by your API. By default, this will be any subtypes in the same assembly as the base type. + /// To override this, you can provide a custom selector function. This setting is only applicable when used in conjunction with + /// UseOneOfForPolymorphism and/or UseAllOfForInheritance. + /// + /// + /// + public static void SelectSubTypesUsing( + this SwaggerGenOptions swaggerGenOptions, + Func> customSelector) + { + swaggerGenOptions.SchemaGeneratorOptions.SubTypesSelector = customSelector; + } + + /// + /// If the configured serializer supports polymorphic serialization/deserialization by emitting/accepting a special "discriminator" property, + /// and UseOneOfForPolymorphism is enabled, then Swashbuckle will include a description for that property based on the serializer's behavior. + /// However, if you've customized your serializer to support polymorphism, you can provide a custom strategy to tell Swashbuckle which property, + /// for a given type, will be used as a discriminator. This setting is only applicable when used in conjunction with UseOneOfForPolymorphism. + /// + /// + /// + public static void SelectDiscriminatorNameUsing( + this SwaggerGenOptions swaggerGenOptions, + Func customSelector) + { + swaggerGenOptions.SchemaGeneratorOptions.DiscriminatorNameSelector = customSelector; + } + + /// + /// If the configured serializer supports polymorphic serialization/deserialization by emitting/accepting a special "discriminator" property, + /// and UseOneOfForPolymorphism is enabled, then Swashbuckle will include a mapping of possible discriminator values to schema definitions. + /// However, if you've customized your serializer to support polymorphism, you can provide a custom mapping strategy to tell Swashbuckle what + /// the discriminator value should be for a given sub type. This setting is only applicable when used in conjunction with UseOneOfForPolymorphism. + /// + /// + /// + public static void SelectDiscriminatorValueUsing( + this SwaggerGenOptions swaggerGenOptions, + Func customSelector) + { + swaggerGenOptions.SchemaGeneratorOptions.DiscriminatorValueSelector = customSelector; + } + + /// + /// Extend reference schemas (using the allOf construct) so that contextual metadata can be applied to all parameter and property schemas + /// + /// + public static void UseAllOfToExtendReferenceSchemas(this SwaggerGenOptions swaggerGenOptions) + { + swaggerGenOptions.SchemaGeneratorOptions.UseAllOfToExtendReferenceSchemas = true; + } + + /// + /// Enable detection of non nullable reference types to set Nullable flag accordingly on schema properties + /// + /// + public static void SupportNonNullableReferenceTypes(this SwaggerGenOptions swaggerGenOptions) + { + swaggerGenOptions.SchemaGeneratorOptions.SupportNonNullableReferenceTypes = true; + } + + /// + /// Extend the Swagger Generator with "filters" that can modify Schemas after they're initially generated + /// + /// A type that derives from ISchemaFilter + /// + /// Optionally inject parameters through filter constructors + public static void SchemaFilter( + this SwaggerGenOptions swaggerGenOptions, + params object[] arguments) + where TFilter : ISchemaFilter + { + swaggerGenOptions.SchemaFilterDescriptors.Add(new FilterDescriptor + { + Type = typeof(TFilter), + Arguments = arguments + }); + } + + /// + /// Extend the Swagger Generator with "filters" that can modify Parameters after they're initially generated + /// + /// A type that derives from IParameterFilter + /// + /// Optionally inject parameters through filter constructors + public static void ParameterFilter( + this SwaggerGenOptions swaggerGenOptions, + params object[] arguments) + where TFilter : IParameterFilter + { + swaggerGenOptions.ParameterFilterDescriptors.Add(new FilterDescriptor + { + Type = typeof(TFilter), + Arguments = arguments + }); + } + + /// + /// Extend the Swagger Generator with "filters" that can modify RequestBodys after they're initially generated + /// + /// A type that derives from IRequestBodyFilter + /// + /// Optionally inject parameters through filter constructors + public static void RequestBodyFilter( + this SwaggerGenOptions swaggerGenOptions, + params object[] arguments) + where TFilter : IRequestBodyFilter + { + swaggerGenOptions.RequestBodyFilterDescriptors.Add(new FilterDescriptor + { + Type = typeof(TFilter), + Arguments = arguments + }); + } + + /// + /// Extend the Swagger Generator with "filters" that can modify Operations after they're initially generated + /// + /// A type that derives from IOperationFilter + /// + /// Optionally inject parameters through filter constructors + public static void OperationFilter( + this SwaggerGenOptions swaggerGenOptions, + params object[] arguments) + where TFilter : IOperationFilter + { + swaggerGenOptions.OperationFilterDescriptors.Add(new FilterDescriptor + { + Type = typeof(TFilter), + Arguments = arguments + }); + } + + public static void GenerateSwaggerDoc( +this SwaggerGenOptions swaggerGenOptions, IEnumerable entries) + { + + var result = new Dictionary(); + var assemblies = entries.Select(p => p.Type.Assembly).Distinct(); + foreach (var assembly in assemblies) + { + var version = assembly + .GetCustomAttributes(true) + .OfType().FirstOrDefault(); + + var title = assembly + .GetCustomAttributes(true) + .OfType().FirstOrDefault(); + + var des = assembly + .GetCustomAttributes(true) + .OfType().FirstOrDefault(); + + if (version == null || title == null) + continue; + swaggerGenOptions.SwaggerDoc(title.Title, new OpenApiInfo() + { + Title = title.Title, + Version = version.Version, + Description = des?.Description, + + }); + } + } + + /// + /// Extend the Swagger Generator with "filters" that can modify SwaggerDocuments after they're initially generated + /// + /// A type that derives from IDocumentFilter + /// + /// Optionally inject parameters through filter constructors + public static void DocumentFilter( + this SwaggerGenOptions swaggerGenOptions, + params object[] arguments) + where TFilter : IDocumentFilter + { + swaggerGenOptions.DocumentFilterDescriptors.Add(new FilterDescriptor + { + Type = typeof(TFilter), + Arguments = arguments + }); + } + + /// + /// Inject human-friendly descriptions for Operations, Parameters and Schemas based on XML Comment files + /// + /// + /// A factory method that returns XML Comments as an XPathDocument + /// + /// Flag to indicate if controller XML comments (i.e. summary) should be used to assign Tag descriptions. + /// Don't set this flag if you're customizing the default tag for operations via TagActionsBy. + /// + public static void IncludeXmlComments( + this SwaggerGenOptions swaggerGenOptions, + Func xmlDocFactory, + bool includeControllerXmlComments = false) + { + var xmlDoc = xmlDocFactory(); + swaggerGenOptions.ParameterFilter(xmlDoc); + swaggerGenOptions.RequestBodyFilter(xmlDoc); + swaggerGenOptions.OperationFilter(xmlDoc); + swaggerGenOptions.SchemaFilter(xmlDoc); + + if (includeControllerXmlComments) + swaggerGenOptions.DocumentFilter(xmlDoc); + } + + /// + /// Inject human-friendly descriptions for Operations, Parameters and Schemas based on XML Comment files + /// + /// + /// An absolute path to the file that contains XML Comments + /// + /// Flag to indicate if controller XML comments (i.e. summary) should be used to assign Tag descriptions. + /// Don't set this flag if you're customizing the default tag for operations via TagActionsBy. + /// + public static void IncludeXmlComments( + this SwaggerGenOptions swaggerGenOptions, + string filePath, + bool includeControllerXmlComments = false) + { + swaggerGenOptions.IncludeXmlComments(() => new XPathDocument(filePath), includeControllerXmlComments); + } + + /// + /// Generate polymorphic schemas (i.e. "oneOf") based on discovered subtypes. + /// Deprecated: Use the \"UseOneOfForPolymorphism\" and \"UseAllOfForInheritance\" settings instead + /// + /// + /// + /// + [Obsolete("You can use \"UseOneOfForPolymorphism\", \"UseAllOfForInheritance\" and \"SelectSubTypesUsing\" to configure equivalent behavior")] + public static void GeneratePolymorphicSchemas( + this SwaggerGenOptions swaggerGenOptions, + Func> subTypesResolver = null, + Func discriminatorSelector = null) + { + swaggerGenOptions.UseOneOfForPolymorphism(); + swaggerGenOptions.UseAllOfForInheritance(); + + if (subTypesResolver != null) + { + swaggerGenOptions.SelectSubTypesUsing(subTypesResolver); + } + + if (discriminatorSelector != null) + { + swaggerGenOptions.SelectDiscriminatorNameUsing(discriminatorSelector); + } + } + } +} diff --git a/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/DependencyInjection/SwaggerGenServiceCollectionExtensions.cs b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/DependencyInjection/SwaggerGenServiceCollectionExtensions.cs new file mode 100644 index 000000000..b9ee95119 --- /dev/null +++ b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/DependencyInjection/SwaggerGenServiceCollectionExtensions.cs @@ -0,0 +1,60 @@ +using System; +using System.Text.Json; +using Microsoft.AspNetCore.Mvc; +using Surging.Core.Swagger_V5.ApiDescriptions; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; +using Surging.Core.Swagger_V5.SwaggerGen; +using Microsoft.Extensions.DependencyInjection; +using Surging.Core.Swagger_V5.Swagger; + +namespace Surging.Core.Swagger_V5.DependencyInjection +{ + public static class SwaggerGenServiceCollectionExtensions + { + public static IServiceCollection AddSwaggerGen( + this IServiceCollection services, + Action setupAction = null) + { + // Add Mvc convention to ensure ApiExplorer is enabled for all actions + services.Configure(c => + c.Conventions.Add(new SwaggerApplicationConvention())); + + // Register custom configurators that takes values from SwaggerGenOptions (i.e. high level config) + // and applies them to SwaggerGeneratorOptions and SchemaGeneratorOptoins (i.e. lower-level config) + services.AddTransient, ConfigureSwaggerGeneratorOptions>(); + services.AddTransient, ConfigureSchemaGeneratorOptions>(); + + // Register generator and it's dependencies + services.TryAddTransient(); + services.TryAddTransient(s => s.GetRequiredService>().Value); + services.TryAddTransient(); + services.TryAddTransient(s => s.GetRequiredService>().Value); + services.TryAddTransient(s => + { +#if (!NETSTANDARD2_0) + var serializerOptions = s.GetService>()?.Value?.JsonSerializerOptions + ?? new JsonSerializerOptions(); +#else + var serializerOptions = new JsonSerializerOptions(); +#endif + + return new JsonSerializerDataContractResolver(serializerOptions); + }); + + // Used by the dotnet-getdocument tool from the Microsoft.Extensions.ApiDescription.Server package. + services.TryAddSingleton(); + + if (setupAction != null) services.ConfigureSwaggerGen(setupAction); + + return services; + } + + public static void ConfigureSwaggerGen( + this IServiceCollection services, + Action setupAction) + { + services.Configure(setupAction); + } + } +} diff --git a/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SchemaGenerator/ISchemaFilter.cs b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SchemaGenerator/ISchemaFilter.cs new file mode 100644 index 000000000..48ab83cd6 --- /dev/null +++ b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SchemaGenerator/ISchemaFilter.cs @@ -0,0 +1,40 @@ +using System; +using System.Reflection; +using Microsoft.OpenApi.Models; + +namespace Surging.Core.Swagger_V5.SwaggerGen +{ + public interface ISchemaFilter + { + void Apply(OpenApiSchema schema, SchemaFilterContext context); + } + + public class SchemaFilterContext + { + public SchemaFilterContext( + Type type, + ISchemaGenerator schemaGenerator, + SchemaRepository schemaRepository, + MemberInfo memberInfo = null, + ParameterInfo parameterInfo = null) + { + Type = type; + SchemaGenerator = schemaGenerator; + SchemaRepository = schemaRepository; + MemberInfo = memberInfo; + ParameterInfo = parameterInfo; + } + + public Type Type { get; } + + public ISchemaGenerator SchemaGenerator { get; } + + public SchemaRepository SchemaRepository { get; } + + public MemberInfo MemberInfo { get; } + + public ParameterInfo ParameterInfo { get; } + + public string DocumentName => SchemaRepository.DocumentName; + } +} \ No newline at end of file diff --git a/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SchemaGenerator/ISerializerDataContractResolver.cs b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SchemaGenerator/ISerializerDataContractResolver.cs new file mode 100644 index 000000000..cc90ace07 --- /dev/null +++ b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SchemaGenerator/ISerializerDataContractResolver.cs @@ -0,0 +1,179 @@ +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace Surging.Core.Swagger_V5.SwaggerGen +{ + public interface ISerializerDataContractResolver + { + DataContract GetDataContractForType(Type type); + } + + public class DataContract + { + public static DataContract ForPrimitive( + Type underlyingType, + DataType dataType, + string dataFormat, + Func jsonConverter = null) + { + return new DataContract( + underlyingType: underlyingType, + dataType: dataType, + dataFormat: dataFormat, + jsonConverter: jsonConverter); + } + + public static DataContract ForArray( + Type underlyingType, + Type itemType, + Func jsonConverter = null) + { + return new DataContract( + underlyingType: underlyingType, + dataType: DataType.Array, + arrayItemType: itemType, + jsonConverter: jsonConverter); + } + + public static DataContract ForDictionary( + Type underlyingType, + Type valueType, + IEnumerable keys = null, + Func jsonConverter = null) + { + return new DataContract( + underlyingType: underlyingType, + dataType: DataType.Dictionary, + dictionaryValueType: valueType, + dictionaryKeys: keys, + jsonConverter: jsonConverter); + } + + public static DataContract ForObject( + Type underlyingType, + IEnumerable properties, + Type extensionDataType = null, + string typeNameProperty = null, + string typeNameValue = null, + Func jsonConverter = null) + { + return new DataContract( + underlyingType: underlyingType, + dataType: DataType.Object, + objectProperties: properties, + objectExtensionDataType: extensionDataType, + objectTypeNameProperty: typeNameProperty, + objectTypeNameValue: typeNameValue, + jsonConverter: jsonConverter); + } + + public static DataContract ForDynamic( + Type underlyingType, + Func jsonConverter = null) + { + return new DataContract( + underlyingType: underlyingType, + dataType: DataType.Unknown, + jsonConverter: jsonConverter); + } + + [Obsolete("Provide jsonConverter function instead of enumValues")] + public static DataContract ForPrimitive( + Type underlyingType, + DataType dataType, + string dataFormat, + IEnumerable enumValues) + { + return new DataContract( + underlyingType: underlyingType, + dataType: dataType, + dataFormat: dataFormat, + enumValues: enumValues); + } + + private DataContract( + Type underlyingType, + DataType dataType, + string dataFormat = null, + IEnumerable enumValues = null, + Type arrayItemType = null, + Type dictionaryValueType = null, + IEnumerable dictionaryKeys = null, + IEnumerable objectProperties = null, + Type objectExtensionDataType = null, + string objectTypeNameProperty = null, + string objectTypeNameValue = null, + Func jsonConverter = null) + { + UnderlyingType = underlyingType; + DataType = dataType; + DataFormat = dataFormat; + EnumValues = enumValues; + ArrayItemType = arrayItemType; + DictionaryValueType = dictionaryValueType; + DictionaryKeys = dictionaryKeys; + ObjectProperties = objectProperties; + ObjectExtensionDataType = objectExtensionDataType; + ObjectTypeNameProperty = objectTypeNameProperty; + ObjectTypeNameValue = objectTypeNameValue; + JsonConverter = jsonConverter ?? new Func(obj => null); + } + + public Type UnderlyingType { get; } + public DataType DataType { get; } + public string DataFormat { get; } + public Type ArrayItemType { get; } + public Type DictionaryValueType { get; } + public IEnumerable DictionaryKeys { get; } + public IEnumerable ObjectProperties { get; } + public Type ObjectExtensionDataType { get; } + public string ObjectTypeNameProperty { get; } + public string ObjectTypeNameValue { get; } + public Func JsonConverter { get; } + + [Obsolete("Use JsonConverter")] + public IEnumerable EnumValues { get; } + } + + public enum DataType + { + Boolean, + Integer, + Number, + String, + Array, + Dictionary, + Object, + Unknown + } + + public class DataProperty + { + public DataProperty( + string name, + Type memberType, + bool isRequired = false, + bool isNullable = false, + bool isReadOnly = false, + bool isWriteOnly = false, + MemberInfo memberInfo = null) + { + Name = name; + IsRequired = isRequired; + IsNullable = isNullable; + IsReadOnly = isReadOnly; + IsWriteOnly = isWriteOnly; + MemberType = memberType; + MemberInfo = memberInfo; + } + + public string Name { get; } + public bool IsRequired { get; } + public bool IsNullable { get; } + public bool IsReadOnly { get; } + public bool IsWriteOnly { get; } + public Type MemberType { get; } + public MemberInfo MemberInfo { get; } + } +} \ No newline at end of file diff --git a/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SchemaGenerator/JsonSerializerDataContractResolver.cs b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SchemaGenerator/JsonSerializerDataContractResolver.cs new file mode 100644 index 000000000..da09e6588 --- /dev/null +++ b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SchemaGenerator/JsonSerializerDataContractResolver.cs @@ -0,0 +1,247 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Surging.Core.Swagger_V5.SwaggerGen +{ + public class JsonSerializerDataContractResolver : ISerializerDataContractResolver + { + private readonly JsonSerializerOptions _serializerOptions; + + public JsonSerializerDataContractResolver(JsonSerializerOptions serializerOptions) + { + _serializerOptions = serializerOptions; + } + + public DataContract GetDataContractForType(Type type) + { + if (type.IsOneOf(typeof(object), typeof(JsonDocument), typeof(JsonElement))) + { + return DataContract.ForDynamic( + underlyingType: type, + jsonConverter: JsonConverterFunc); + } + + if (PrimitiveTypesAndFormats.ContainsKey(type)) + { + var primitiveTypeAndFormat = PrimitiveTypesAndFormats[type]; + + return DataContract.ForPrimitive( + underlyingType: type, + dataType: primitiveTypeAndFormat.Item1, + dataFormat: primitiveTypeAndFormat.Item2, + jsonConverter: JsonConverterFunc); + } + + if (type.IsEnum) + { + var enumValues = type.GetEnumValues(); + + //Test to determine if the serializer will treat as string + var serializeAsString = (enumValues.Length > 0) + && JsonConverterFunc(enumValues.GetValue(0)).StartsWith("\""); + + var primitiveTypeAndFormat = serializeAsString + ? PrimitiveTypesAndFormats[typeof(string)] + : PrimitiveTypesAndFormats[type.GetEnumUnderlyingType()]; + + return DataContract.ForPrimitive( + underlyingType: type, + dataType: primitiveTypeAndFormat.Item1, + dataFormat: primitiveTypeAndFormat.Item2, + jsonConverter: JsonConverterFunc); + } + + if (IsSupportedDictionary(type, out Type keyType, out Type valueType)) + { + return DataContract.ForDictionary( + underlyingType: type, + valueType: valueType, + keys: null, // STJ doesn't currently support dictionaries with enum key types + jsonConverter: JsonConverterFunc); + } + + if (IsSupportedCollection(type, out Type itemType)) + { + return DataContract.ForArray( + underlyingType: type, + itemType: itemType, + jsonConverter: JsonConverterFunc); + } + + return DataContract.ForObject( + underlyingType: type, + properties: GetDataPropertiesFor(type, out Type extensionDataType), + extensionDataType: extensionDataType, + jsonConverter: JsonConverterFunc); + } + + private string JsonConverterFunc(object value) + { + return JsonSerializer.Serialize(value, _serializerOptions); + } + + public bool IsSupportedDictionary(Type type, out Type keyType, out Type valueType) + { + if (type.IsConstructedFrom(typeof(IDictionary<,>), out Type constructedType) + || type.IsConstructedFrom(typeof(IReadOnlyDictionary<,>), out constructedType)) + { + keyType = constructedType.GenericTypeArguments[0]; + valueType = constructedType.GenericTypeArguments[1]; + return true; + } + + if (typeof(IDictionary).IsAssignableFrom(type)) + { + keyType = valueType = typeof(object); + return true; + } + + keyType = valueType = null; + return false; + } + + public bool IsSupportedCollection(Type type, out Type itemType) + { + if (type.IsConstructedFrom(typeof(IEnumerable<>), out Type constructedType)) + { + itemType = constructedType.GenericTypeArguments[0]; + return true; + } + +#if (!NETSTANDARD2_0) + if (type.IsConstructedFrom(typeof(IAsyncEnumerable<>), out constructedType)) + { + itemType = constructedType.GenericTypeArguments[0]; + return true; + } +#endif + + if (typeof(IEnumerable).IsAssignableFrom(type)) + { + itemType = typeof(object); + return true; + } + + itemType = null; + return false; + } + + private IEnumerable GetDataPropertiesFor(Type objectType, out Type extensionDataType) + { + extensionDataType = null; + + const BindingFlags PublicBindingAttr = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; + + var publicProperties = objectType.IsInterface + ? new[] { objectType }.Concat(objectType.GetInterfaces()).SelectMany(i => i.GetProperties(PublicBindingAttr)) + : objectType.GetProperties(PublicBindingAttr); + + var applicableProperties = publicProperties + .Where(property => + { + // .NET 5 introduces JsonIgnoreAttribute.Condition which should be honored + bool isIgnoredViaNet5Attribute = true; + +#if NET5_0_OR_GREATER + JsonIgnoreAttribute jsonIgnoreAttribute = property.GetCustomAttribute(); + if (jsonIgnoreAttribute != null) + { + isIgnoredViaNet5Attribute = jsonIgnoreAttribute.Condition switch + { + JsonIgnoreCondition.Never => false, + JsonIgnoreCondition.Always => true, + JsonIgnoreCondition.WhenWritingDefault => false, + JsonIgnoreCondition.WhenWritingNull => false, + _ => true + }; + } +#endif + + return + (property.IsPubliclyReadable() || property.IsPubliclyWritable()) && + !(property.GetIndexParameters().Any()) && + !(property.HasAttribute() && isIgnoredViaNet5Attribute) && + !(_serializerOptions.IgnoreReadOnlyProperties && !property.IsPubliclyWritable()); + }) + .OrderBy(property => property.DeclaringType.GetInheritanceChain().Length); + + var dataProperties = new List(); + + foreach (var propertyInfo in applicableProperties) + { + if (propertyInfo.HasAttribute() + && propertyInfo.PropertyType.IsConstructedFrom(typeof(IDictionary<,>), out Type constructedDictionary)) + { + extensionDataType = constructedDictionary.GenericTypeArguments[1]; + continue; + } + + var name = propertyInfo.GetCustomAttribute()?.Name + ?? _serializerOptions.PropertyNamingPolicy?.ConvertName(propertyInfo.Name) ?? propertyInfo.Name; + + // .NET 5 introduces support for serializing immutable types via parameterized constructors + // See https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-immutability?pivots=dotnet-6-0 + var isDeserializedViaConstructor = false; + +#if NET5_0_OR_GREATER + var deserializationConstructor = propertyInfo.DeclaringType?.GetConstructors() + .OrderBy(c => + { + if (c.GetCustomAttribute() != null) return 1; + if (c.GetParameters().Length == 0) return 2; + return 3; + }) + .FirstOrDefault(); + + isDeserializedViaConstructor = deserializationConstructor != null && deserializationConstructor.GetParameters() + .Any(p => + { + return + string.Equals(p.Name, propertyInfo.Name, StringComparison.OrdinalIgnoreCase) || + string.Equals(p.Name, name, StringComparison.OrdinalIgnoreCase); + }); +#endif + + dataProperties.Add( + new DataProperty( + name: name, + isRequired: false, + isNullable: propertyInfo.PropertyType.IsReferenceOrNullableType(), + isReadOnly: propertyInfo.IsPubliclyReadable() && !propertyInfo.IsPubliclyWritable() && !isDeserializedViaConstructor, + isWriteOnly: propertyInfo.IsPubliclyWritable() && !propertyInfo.IsPubliclyReadable(), + memberType: propertyInfo.PropertyType, + memberInfo: propertyInfo)); + } + + return dataProperties; + } + + private static readonly Dictionary> PrimitiveTypesAndFormats = new Dictionary> + { + [ typeof(bool) ] = Tuple.Create(DataType.Boolean, (string)null), + [ typeof(byte) ] = Tuple.Create(DataType.Integer, "int32"), + [ typeof(sbyte) ] = Tuple.Create(DataType.Integer, "int32"), + [ typeof(short) ] = Tuple.Create(DataType.Integer, "int32"), + [ typeof(ushort) ] = Tuple.Create(DataType.Integer, "int32"), + [ typeof(int) ] = Tuple.Create(DataType.Integer, "int32"), + [ typeof(uint) ] = Tuple.Create(DataType.Integer, "int32"), + [ typeof(long) ] = Tuple.Create(DataType.Integer, "int64"), + [ typeof(ulong) ] = Tuple.Create(DataType.Integer, "int64"), + [ typeof(float) ] = Tuple.Create(DataType.Number, "float"), + [ typeof(double) ] = Tuple.Create(DataType.Number, "double"), + [ typeof(decimal) ] = Tuple.Create(DataType.Number, "double"), + [ typeof(byte[]) ] = Tuple.Create(DataType.String, "byte"), + [ typeof(string) ] = Tuple.Create(DataType.String, (string)null), + [ typeof(char) ] = Tuple.Create(DataType.String, (string)null), + [ typeof(DateTime) ] = Tuple.Create(DataType.String, "date-time"), + [ typeof(DateTimeOffset) ] = Tuple.Create(DataType.String, "date-time"), + [ typeof(Guid) ] = Tuple.Create(DataType.String, "uuid"), + [ typeof(Uri) ] = Tuple.Create(DataType.String, "uri") + }; + } +} diff --git a/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SchemaGenerator/MemberInfoExtensions.cs b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SchemaGenerator/MemberInfoExtensions.cs new file mode 100644 index 000000000..43365477f --- /dev/null +++ b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SchemaGenerator/MemberInfoExtensions.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Microsoft.AspNetCore.Mvc; + +namespace Surging.Core.Swagger_V5.SwaggerGen +{ + public static class MemberInfoExtensions + { + private const string NullableAttributeFullTypeName = "System.Runtime.CompilerServices.NullableAttribute"; + private const string NullableFlagsFieldName = "NullableFlags"; + private const string NullableContextAttributeFullTypeName = "System.Runtime.CompilerServices.NullableContextAttribute"; + private const string FlagFieldName = "Flag"; + + public static IEnumerable GetInlineAndMetadataAttributes(this MemberInfo memberInfo) + { + var attributes = memberInfo.GetCustomAttributes(true) + .ToList(); + + var metadataTypeAttribute = memberInfo.DeclaringType.GetCustomAttributes(true) + .OfType() + .FirstOrDefault(); + + var metadataMemberInfo = metadataTypeAttribute?.MetadataType.GetMember(memberInfo.Name) + .FirstOrDefault(); + + if (metadataMemberInfo != null) + { + attributes.AddRange(metadataMemberInfo.GetCustomAttributes(true)); + } + + return attributes; + } + + public static bool IsNonNullableReferenceType(this MemberInfo memberInfo) + { + var memberType = memberInfo.MemberType == MemberTypes.Field + ? ((FieldInfo)memberInfo).FieldType + : ((PropertyInfo)memberInfo).PropertyType; + + if (memberType.IsValueType) return false; + + var nullableAttribute = memberInfo.GetNullableAttribute(); + + if (nullableAttribute == null) + { + return memberInfo.GetNullableFallbackValue(); + } + + if (nullableAttribute.GetType().GetField(NullableFlagsFieldName) is FieldInfo field && + field.GetValue(nullableAttribute) is byte[] flags && + flags.Length >= 1 && flags[0] == 1) + { + return true; + } + + return false; + } + + private static object GetNullableAttribute(this MemberInfo memberInfo) + { + var nullableAttribute = memberInfo.GetCustomAttributes() + .Where(attr => string.Equals(attr.GetType().FullName, NullableAttributeFullTypeName)) + .FirstOrDefault(); + + return nullableAttribute; + } + + private static bool GetNullableFallbackValue(this MemberInfo memberInfo) + { + var declaringTypes = memberInfo.DeclaringType.IsNested + ? new Type[] { memberInfo.DeclaringType, memberInfo.DeclaringType.DeclaringType } + : new Type[] { memberInfo.DeclaringType }; + + foreach (var declaringType in declaringTypes) + { + var attributes = (IEnumerable)declaringType.GetCustomAttributes(false); + + var nullableContext = attributes + .Where(attr => string.Equals(attr.GetType().FullName, NullableContextAttributeFullTypeName)) + .FirstOrDefault(); + + if (nullableContext != null) + { + if (nullableContext.GetType().GetField(FlagFieldName) is FieldInfo field && + field.GetValue(nullableContext) is byte flag && flag == 1) + { + return true; + } + else + { + return false; + } + } + } + + return false; + } + } +} diff --git a/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SchemaGenerator/OpenApiSchemaExtensions.cs b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SchemaGenerator/OpenApiSchemaExtensions.cs new file mode 100644 index 000000000..d6c82f250 --- /dev/null +++ b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SchemaGenerator/OpenApiSchemaExtensions.cs @@ -0,0 +1,113 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using Microsoft.OpenApi.Models; +using AnnotationsDataType = System.ComponentModel.DataAnnotations.DataType; + +namespace Surging.Core.Swagger_V5.SwaggerGen +{ + public static class OpenApiSchemaExtensions + { + public static void ApplyValidationAttributes(this OpenApiSchema schema, IEnumerable customAttributes) + { + foreach (var attribute in customAttributes) + { + if (attribute is DataTypeAttribute dataTypeAttribute) + ApplyDataTypeAttribute(schema, dataTypeAttribute); + + else if (attribute is MinLengthAttribute minLengthAttribute) + ApplyMinLengthAttribute(schema, minLengthAttribute); + + else if (attribute is MaxLengthAttribute maxLengthAttribute) + ApplyMaxLengthAttribute(schema, maxLengthAttribute); + + else if (attribute is RangeAttribute rangeAttribute) + ApplyRangeAttribute(schema, rangeAttribute); + + else if (attribute is RegularExpressionAttribute regularExpressionAttribute) + ApplyRegularExpressionAttribute(schema, regularExpressionAttribute); + + else if (attribute is StringLengthAttribute stringLengthAttribute) + ApplyStringLengthAttribute(schema, stringLengthAttribute); + } + } + + public static string ResolveType(this OpenApiSchema schema, SchemaRepository schemaRepository) + { + if (schema.Reference != null && schemaRepository.Schemas.TryGetValue(schema.Reference.Id, out OpenApiSchema definitionSchema)) + return definitionSchema.ResolveType(schemaRepository); + + foreach (var subSchema in schema.AllOf) + { + var type = subSchema.ResolveType(schemaRepository); + if (type != null) return type; + } + + return schema.Type; + } + + private static void ApplyDataTypeAttribute(OpenApiSchema schema, DataTypeAttribute dataTypeAttribute) + { + var formats = new Dictionary + { + { AnnotationsDataType.DateTime, "date-time" }, + { AnnotationsDataType.Date, "date" }, + { AnnotationsDataType.Time, "time" }, + { AnnotationsDataType.Duration, "duration" }, + { AnnotationsDataType.PhoneNumber, "tel" }, + { AnnotationsDataType.Currency, "currency" }, + { AnnotationsDataType.Text, "string" }, + { AnnotationsDataType.Html, "html" }, + { AnnotationsDataType.MultilineText, "multiline" }, + { AnnotationsDataType.EmailAddress, "email" }, + { AnnotationsDataType.Password, "password" }, + { AnnotationsDataType.Url, "uri" }, + { AnnotationsDataType.ImageUrl, "uri" }, + { AnnotationsDataType.CreditCard, "credit-card" }, + { AnnotationsDataType.PostalCode, "postal-code" } + }; + + if (formats.TryGetValue(dataTypeAttribute.DataType, out string format)) + { + schema.Format = format; + } + } + + private static void ApplyMinLengthAttribute(OpenApiSchema schema, MinLengthAttribute minLengthAttribute) + { + if (schema.Type == "array") + schema.MinItems = minLengthAttribute.Length; + else + schema.MinLength = minLengthAttribute.Length; + } + + private static void ApplyMaxLengthAttribute(OpenApiSchema schema, MaxLengthAttribute maxLengthAttribute) + { + if (schema.Type == "array") + schema.MaxItems = maxLengthAttribute.Length; + else + schema.MaxLength = maxLengthAttribute.Length; + } + + private static void ApplyRangeAttribute(OpenApiSchema schema, RangeAttribute rangeAttribute) + { + schema.Maximum = decimal.TryParse(rangeAttribute.Maximum.ToString(), out decimal maximum) + ? maximum + : schema.Maximum; + + schema.Minimum = decimal.TryParse(rangeAttribute.Minimum.ToString(), out decimal minimum) + ? minimum + : schema.Minimum; + } + + private static void ApplyRegularExpressionAttribute(OpenApiSchema schema, RegularExpressionAttribute regularExpressionAttribute) + { + schema.Pattern = regularExpressionAttribute.Pattern; + } + + private static void ApplyStringLengthAttribute(OpenApiSchema schema, StringLengthAttribute stringLengthAttribute) + { + schema.MinLength = stringLengthAttribute.MinimumLength; + schema.MaxLength = stringLengthAttribute.MaximumLength; + } + } +} diff --git a/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SchemaGenerator/PropertyInfoExtensions.cs b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SchemaGenerator/PropertyInfoExtensions.cs new file mode 100644 index 000000000..d9c5d4b12 --- /dev/null +++ b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SchemaGenerator/PropertyInfoExtensions.cs @@ -0,0 +1,24 @@ +using System; +using System.Reflection; + +namespace Surging.Core.Swagger_V5.SwaggerGen +{ + public static class PropertyInfoExtensions + { + public static bool HasAttribute(this PropertyInfo property) + where TAttribute : Attribute + { + return property.GetCustomAttribute() != null; + } + + public static bool IsPubliclyReadable(this PropertyInfo property) + { + return property.GetMethod?.IsPublic == true; + } + + public static bool IsPubliclyWritable(this PropertyInfo property) + { + return property.SetMethod?.IsPublic == true; + } + } +} diff --git a/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SchemaGenerator/SchemaGenerator.cs b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SchemaGenerator/SchemaGenerator.cs new file mode 100644 index 000000000..28595b59b --- /dev/null +++ b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SchemaGenerator/SchemaGenerator.cs @@ -0,0 +1,475 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Text.Json; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.OpenApi.Models; + +namespace Surging.Core.Swagger_V5.SwaggerGen +{ + public class SchemaGenerator : ISchemaGenerator + { + private readonly SchemaGeneratorOptions _generatorOptions; + private readonly ISerializerDataContractResolver _serializerDataContractResolver; + + public SchemaGenerator(SchemaGeneratorOptions generatorOptions, ISerializerDataContractResolver serializerDataContractResolver) + { + _generatorOptions = generatorOptions; + _serializerDataContractResolver = serializerDataContractResolver; + } + + public OpenApiSchema GenerateSchema( + Type modelType, + SchemaRepository schemaRepository, + MemberInfo memberInfo = null, + ParameterInfo parameterInfo = null) + { + if (memberInfo != null) + return GenerateSchemaForMember(modelType, schemaRepository, memberInfo); + + if (parameterInfo != null) + return GenerateSchemaForParameter(modelType, schemaRepository, parameterInfo); + + return GenerateSchemaForType(modelType, schemaRepository); + } + + private OpenApiSchema GenerateSchemaForMember( + Type modelType, + SchemaRepository schemaRepository, + MemberInfo memberInfo, + DataProperty dataProperty = null) + { + var dataContract = GetDataContractFor(modelType); + + var schema = _generatorOptions.UseOneOfForPolymorphism && IsBaseTypeWithKnownTypesDefined(dataContract, out var knownTypesDataContracts) + ? GeneratePolymorphicSchema(dataContract, schemaRepository, knownTypesDataContracts) + : GenerateConcreteSchema(dataContract, schemaRepository); + + if (_generatorOptions.UseAllOfToExtendReferenceSchemas && schema.Reference != null) + { + schema.AllOf = new[] { new OpenApiSchema { Reference = schema.Reference } }; + schema.Reference = null; + } + + if (schema.Reference == null) + { + var customAttributes = memberInfo.GetInlineAndMetadataAttributes(); + + // Nullable, ReadOnly & WriteOnly are only relevant for Schema "properties" (i.e. where dataProperty is non-null) + if (dataProperty != null) + { + schema.Nullable = _generatorOptions.SupportNonNullableReferenceTypes + ? dataProperty.IsNullable && !customAttributes.OfType().Any() && !memberInfo.IsNonNullableReferenceType() + : dataProperty.IsNullable && !customAttributes.OfType().Any(); + + schema.ReadOnly = dataProperty.IsReadOnly; + schema.WriteOnly = dataProperty.IsWriteOnly; + } + + var defaultValueAttribute = customAttributes.OfType().FirstOrDefault(); + if (defaultValueAttribute != null) + { + var defaultAsJson = dataContract.JsonConverter(defaultValueAttribute.Value); + schema.Default = OpenApiAnyFactory.CreateFromJson(defaultAsJson); + } + + var obsoleteAttribute = customAttributes.OfType().FirstOrDefault(); + if (obsoleteAttribute != null) + { + schema.Deprecated = true; + } + + schema.ApplyValidationAttributes(customAttributes); + + ApplyFilters(schema, modelType, schemaRepository, memberInfo: memberInfo); + } + + return schema; + } + + private OpenApiSchema GenerateSchemaForParameter( + Type modelType, + SchemaRepository schemaRepository, + ParameterInfo parameterInfo) + { + var dataContract = GetDataContractFor(modelType); + + var schema = _generatorOptions.UseOneOfForPolymorphism && IsBaseTypeWithKnownTypesDefined(dataContract, out var knownTypesDataContracts) + ? GeneratePolymorphicSchema(dataContract, schemaRepository, knownTypesDataContracts) + : GenerateConcreteSchema(dataContract, schemaRepository); + + if (_generatorOptions.UseAllOfToExtendReferenceSchemas && schema.Reference != null) + { + schema.AllOf = new[] { new OpenApiSchema { Reference = schema.Reference } }; + schema.Reference = null; + } + + if (schema.Reference == null) + { + var customAttributes = parameterInfo.GetCustomAttributes(); + + var defaultValue = parameterInfo.HasDefaultValue + ? parameterInfo.DefaultValue + : customAttributes.OfType().FirstOrDefault()?.Value; + + if (defaultValue != null) + { + var defaultAsJson = dataContract.JsonConverter(defaultValue); + schema.Default = OpenApiAnyFactory.CreateFromJson(defaultAsJson); + } + + schema.ApplyValidationAttributes(customAttributes); + + ApplyFilters(schema, modelType, schemaRepository, parameterInfo: parameterInfo); + } + + return schema; + } + + private OpenApiSchema GenerateSchemaForType(Type modelType, SchemaRepository schemaRepository) + { + var dataContract = GetDataContractFor(modelType); + + var schema = _generatorOptions.UseOneOfForPolymorphism && IsBaseTypeWithKnownTypesDefined(dataContract, out var knownTypesDataContracts) + ? GeneratePolymorphicSchema(dataContract, schemaRepository, knownTypesDataContracts) + : GenerateConcreteSchema(dataContract, schemaRepository); + + if (schema.Reference == null) + { + ApplyFilters(schema, modelType, schemaRepository); + } + + return schema; + } + + private DataContract GetDataContractFor(Type modelType) + { + var effectiveType = Nullable.GetUnderlyingType(modelType) ?? modelType; + return _serializerDataContractResolver.GetDataContractForType(effectiveType); + } + + private bool IsBaseTypeWithKnownTypesDefined(DataContract dataContract, out IEnumerable knownTypesDataContracts) + { + knownTypesDataContracts = null; + + if (dataContract.DataType != DataType.Object) return false; + + var subTypes = _generatorOptions.SubTypesSelector(dataContract.UnderlyingType); + + if (!subTypes.Any()) return false; + + var knownTypes = !dataContract.UnderlyingType.IsAbstract + ? new[] { dataContract.UnderlyingType }.Union(subTypes) + : subTypes; + + knownTypesDataContracts = knownTypes.Select(knownType => GetDataContractFor(knownType)); + return true; + } + + private OpenApiSchema GeneratePolymorphicSchema( + DataContract dataContract, + SchemaRepository schemaRepository, + IEnumerable knownTypesDataContracts) + { + return new OpenApiSchema + { + OneOf = knownTypesDataContracts + .Select(allowedTypeDataContract => GenerateConcreteSchema(allowedTypeDataContract, schemaRepository)) + .ToList() + }; + } + + private OpenApiSchema GenerateConcreteSchema(DataContract dataContract, SchemaRepository schemaRepository) + { + if (TryGetCustomTypeMapping(dataContract.UnderlyingType, out Func customSchemaFactory)) + { + return customSchemaFactory(); + } + + if (dataContract.UnderlyingType.IsAssignableToOneOf(typeof(IFormFile), typeof(FileResult))) + { + return new OpenApiSchema { Type = "string", Format = "binary" }; + } + + Func schemaFactory; + bool returnAsReference; + + switch (dataContract.DataType) + { + case DataType.Boolean: + case DataType.Integer: + case DataType.Number: + case DataType.String: + { + schemaFactory = () => CreatePrimitiveSchema(dataContract); + returnAsReference = dataContract.UnderlyingType.IsEnum && !_generatorOptions.UseInlineDefinitionsForEnums; + break; + } + + case DataType.Array: + { + schemaFactory = () => CreateArraySchema(dataContract, schemaRepository); + returnAsReference = dataContract.UnderlyingType == dataContract.ArrayItemType; + break; + } + + case DataType.Dictionary: + { + schemaFactory = () => CreateDictionarySchema(dataContract, schemaRepository); + returnAsReference = dataContract.UnderlyingType == dataContract.DictionaryValueType; + break; + } + + case DataType.Object: + { + schemaFactory = () => CreateObjectSchema(dataContract, schemaRepository); + returnAsReference = true; + break; + } + + default: + { + schemaFactory = () => new OpenApiSchema(); + returnAsReference = false; + break; + } + } + + return returnAsReference + ? GenerateReferencedSchema(dataContract, schemaRepository, schemaFactory) + : schemaFactory(); + } + + private bool TryGetCustomTypeMapping(Type modelType, out Func schemaFactory) + { + return _generatorOptions.CustomTypeMappings.TryGetValue(modelType, out schemaFactory) + || (modelType.IsConstructedGenericType && _generatorOptions.CustomTypeMappings.TryGetValue(modelType.GetGenericTypeDefinition(), out schemaFactory)); + } + + private OpenApiSchema CreatePrimitiveSchema(DataContract dataContract) + { + var schema = new OpenApiSchema + { + Type = dataContract.DataType.ToString().ToLower(CultureInfo.InvariantCulture), + Format = dataContract.DataFormat + }; + + // For backcompat only - EnumValues is obsolete + if (dataContract.EnumValues != null) + { + schema.Enum = dataContract.EnumValues + .Select(value => JsonSerializer.Serialize(value)) + .Select(valueAsJson => OpenApiAnyFactory.CreateFromJson(valueAsJson)) + .ToList(); + + return schema; + } + + if (dataContract.UnderlyingType.IsEnum) + { + schema.Enum = dataContract.UnderlyingType.GetEnumValues() + .Cast() + .Distinct() + .Select(value => dataContract.JsonConverter(value)) + .Select(valueAsJson => OpenApiAnyFactory.CreateFromJson(valueAsJson)) + .ToList(); + } + + return schema; + } + + private OpenApiSchema CreateArraySchema(DataContract dataContract, SchemaRepository schemaRepository) + { + var hasUniqueItems = dataContract.UnderlyingType.IsConstructedFrom(typeof(ISet<>), out _) + || dataContract.UnderlyingType.IsConstructedFrom(typeof(KeyedCollection<,>), out _); + + return new OpenApiSchema + { + Type = "array", + Items = GenerateSchema(dataContract.ArrayItemType, schemaRepository), + UniqueItems = hasUniqueItems ? (bool?)true : null + }; + } + + private OpenApiSchema CreateDictionarySchema(DataContract dataContract, SchemaRepository schemaRepository) + { + if (dataContract.DictionaryKeys != null) + { + // This is a special case where the set of key values is known (e.g. if the key type is an enum) + return new OpenApiSchema + { + Type = "object", + Properties = dataContract.DictionaryKeys.ToDictionary( + name => name, + name => GenerateSchema(dataContract.DictionaryValueType, schemaRepository)), + AdditionalPropertiesAllowed = false, + }; + } + else + { + return new OpenApiSchema + { + Type = "object", + AdditionalPropertiesAllowed = true, + AdditionalProperties = GenerateSchema(dataContract.DictionaryValueType, schemaRepository) + }; + } + } + + private OpenApiSchema CreateObjectSchema(DataContract dataContract, SchemaRepository schemaRepository) + { + var schema = new OpenApiSchema + { + Type = "object", + Properties = new Dictionary(), + Required = new SortedSet(), + AdditionalPropertiesAllowed = false + }; + + var applicableDataProperties = dataContract.ObjectProperties; + + if (_generatorOptions.UseAllOfForInheritance || _generatorOptions.UseOneOfForPolymorphism) + { + if (IsKnownSubType(dataContract, out var baseTypeDataContract)) + { + var baseTypeSchema = GenerateConcreteSchema(baseTypeDataContract, schemaRepository); + + schema.AllOf.Add(baseTypeSchema); + + applicableDataProperties = applicableDataProperties + .Where(dataProperty => dataProperty.MemberInfo.DeclaringType == dataContract.UnderlyingType); + } + + if (IsBaseTypeWithKnownTypesDefined(dataContract, out var knownTypesDataContracts)) + { + foreach (var knownTypeDataContract in knownTypesDataContracts) + { + // Ensure schema is generated for all known types + GenerateConcreteSchema(knownTypeDataContract, schemaRepository); + } + + if (TryGetDiscriminatorFor(dataContract, schemaRepository, knownTypesDataContracts, out var discriminator)) + { + schema.Properties.Add(discriminator.PropertyName, new OpenApiSchema { Type = "string" }); + schema.Required.Add(discriminator.PropertyName); + schema.Discriminator = discriminator; + } + } + } + + foreach (var dataProperty in applicableDataProperties) + { + var customAttributes = dataProperty.MemberInfo?.GetInlineAndMetadataAttributes() ?? Enumerable.Empty(); + + if (_generatorOptions.IgnoreObsoleteProperties && customAttributes.OfType().Any()) + continue; + + schema.Properties[dataProperty.Name] = (dataProperty.MemberInfo != null) + ? GenerateSchemaForMember(dataProperty.MemberType, schemaRepository, dataProperty.MemberInfo, dataProperty) + : GenerateSchemaForType(dataProperty.MemberType, schemaRepository); + + if ((dataProperty.IsRequired || customAttributes.OfType().Any()) + && !schema.Required.Contains(dataProperty.Name)) + { + schema.Required.Add(dataProperty.Name); + } + } + + if (dataContract.ObjectExtensionDataType != null) + { + schema.AdditionalPropertiesAllowed = true; + schema.AdditionalProperties = GenerateSchema(dataContract.ObjectExtensionDataType, schemaRepository); + } + + return schema; + } + + private bool IsKnownSubType(DataContract dataContract, out DataContract baseTypeDataContract) + { + baseTypeDataContract = null; + + var baseType = dataContract.UnderlyingType.BaseType; + + if (baseType == null || baseType == typeof(object) || !_generatorOptions.SubTypesSelector(baseType).Contains(dataContract.UnderlyingType)) + return false; + + baseTypeDataContract = GetDataContractFor(baseType); + return true; + } + + private bool TryGetDiscriminatorFor( + DataContract dataContract, + SchemaRepository schemaRepository, + IEnumerable knownTypesDataContracts, + out OpenApiDiscriminator discriminator) + { + discriminator = null; + + var discriminatorName = _generatorOptions.DiscriminatorNameSelector(dataContract.UnderlyingType) + ?? dataContract.ObjectTypeNameProperty; + + if (discriminatorName == null) return false; + + discriminator = new OpenApiDiscriminator + { + PropertyName = discriminatorName + }; + + foreach (var knownTypeDataContract in knownTypesDataContracts) + { + var discriminatorValue = _generatorOptions.DiscriminatorValueSelector(knownTypeDataContract.UnderlyingType) + ?? knownTypeDataContract.ObjectTypeNameValue; + + if (discriminatorValue == null) continue; + + discriminator.Mapping.Add(discriminatorValue, GenerateConcreteSchema(knownTypeDataContract, schemaRepository).Reference.ReferenceV3); + } + + return true; + } + + private OpenApiSchema GenerateReferencedSchema( + DataContract dataContract, + SchemaRepository schemaRepository, + Func definitionFactory) + { + if (schemaRepository.TryLookupByType(dataContract.UnderlyingType, out OpenApiSchema referenceSchema)) + return referenceSchema; + + var schemaId = _generatorOptions.SchemaIdSelector(dataContract.UnderlyingType); + + schemaRepository.RegisterType(dataContract.UnderlyingType, schemaId); + + var schema = definitionFactory(); + ApplyFilters(schema, dataContract.UnderlyingType, schemaRepository); + + return schemaRepository.AddDefinition(schemaId, schema); + } + + private void ApplyFilters( + OpenApiSchema schema, + Type type, + SchemaRepository schemaRepository, + MemberInfo memberInfo = null, + ParameterInfo parameterInfo = null) + { + var filterContext = new SchemaFilterContext( + type: type, + schemaGenerator: this, + schemaRepository: schemaRepository, + memberInfo: memberInfo, + parameterInfo: parameterInfo); + + foreach (var filter in _generatorOptions.SchemaFilters) + { + filter.Apply(schema, filterContext); + } + } + } +} diff --git a/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SchemaGenerator/SchemaGeneratorOptions.cs b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SchemaGenerator/SchemaGeneratorOptions.cs new file mode 100644 index 000000000..f06c777e5 --- /dev/null +++ b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SchemaGenerator/SchemaGeneratorOptions.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Models; + +namespace Surging.Core.Swagger_V5.SwaggerGen +{ + public class SchemaGeneratorOptions + { + public SchemaGeneratorOptions() + { + CustomTypeMappings = new Dictionary>(); + SubTypesSelector = DefaultSubTypesSelector; + SchemaIdSelector = IgnoreFullyQualified==null? DefaultSchemaIdSelector: (type) => type.FriendlyId(IgnoreFullyQualified.Value); + DiscriminatorNameSelector = DefaultDiscriminatorNameSelector; + DiscriminatorValueSelector = DefaultDiscriminatorValueSelector; + SchemaFilters = new List(); + } + + public IDictionary> CustomTypeMappings { get; set; } + + public bool UseInlineDefinitionsForEnums { get; set; } + + public Func SchemaIdSelector { get; set; } + + public bool IgnoreObsoleteProperties { get; set; } + + public bool UseAllOfForInheritance { get; set; } + + public bool UseOneOfForPolymorphism { get; set; } + + public Func> SubTypesSelector { get; set; } + + public Func DiscriminatorNameSelector { get; set; } + + public Func DiscriminatorValueSelector { get; set; } + + public bool UseAllOfToExtendReferenceSchemas { get; set; } + + public bool SupportNonNullableReferenceTypes { get; set; } + + public IList SchemaFilters { get; set; } + + public bool? IgnoreFullyQualified { get; set; } + + private string DefaultSchemaIdSelector(Type modelType) + { + if (!modelType.IsConstructedGenericType) return modelType.Name.Replace("[]", "Array"); + + var prefix = modelType.GetGenericArguments() + .Select(genericArg => DefaultSchemaIdSelector(genericArg)) + .Aggregate((previous, current) => previous + current); + + return prefix + modelType.Name.Split('`').First(); + } + + private IEnumerable DefaultSubTypesSelector(Type baseType) + { + return baseType.Assembly.GetTypes().Where(type => type.IsSubclassOf(baseType)); + } + + private string DefaultDiscriminatorNameSelector(Type baseType) + { + return null; + } + + private string DefaultDiscriminatorValueSelector(Type subType) + { + return null; + } + } +} \ No newline at end of file diff --git a/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SchemaGenerator/TypeExtensions.cs b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SchemaGenerator/TypeExtensions.cs new file mode 100644 index 000000000..f7f2dea87 --- /dev/null +++ b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SchemaGenerator/TypeExtensions.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace Surging.Core.Swagger_V5.SwaggerGen +{ + public static class TypeExtensions + { + public static string FriendlyId(this Type type, bool fullyQualified = false) + { + var typeName = fullyQualified + ? type.FullNameSansTypeArguments().Replace("+", ".") + : type.Name; + + if (type.GetTypeInfo().IsGenericType) + { + var genericArgumentIds = type.GetGenericArguments() + .Select(t => t.FriendlyId(fullyQualified)) + .ToArray(); + + return new StringBuilder(typeName) + .Replace(string.Format("`{0}", genericArgumentIds.Count()), string.Empty) + .Append(string.Format("[{0}]", string.Join(",", genericArgumentIds).TrimEnd(','))) + .ToString(); + } + + return typeName; + } + public static bool IsOneOf(this Type type, params Type[] possibleTypes) + { + return possibleTypes.Any(possibleType => possibleType == type); + } + + public static bool IsAssignableTo(this Type type, Type baseType) + { + return baseType.IsAssignableFrom(type); + } + + internal static bool IsFSharpOption(this Type type) + { + return type.FullNameSansTypeArguments() == "Microsoft.FSharp.Core.FSharpOption`1"; + } + + public static bool IsAssignableToOneOf(this Type type, params Type[] possibleBaseTypes) + { + return possibleBaseTypes.Any(possibleBaseType => possibleBaseType.IsAssignableFrom(type)); + } + + public static bool IsConstructedFrom(this Type type, Type genericType, out Type constructedType) + { + constructedType = new[] { type } + .Union(type.GetInheritanceChain()) + .Union(type.GetInterfaces()) + .FirstOrDefault(i => i.IsConstructedGenericType && i.GetGenericTypeDefinition() == genericType); + + return (constructedType != null); + } + + private static string FullNameSansTypeArguments(this Type type) + { + if (string.IsNullOrEmpty(type.FullName)) return string.Empty; + + var fullName = type.FullName; + var chopIndex = fullName.IndexOf("[["); + return (chopIndex == -1) ? fullName : fullName.Substring(0, chopIndex); + } + + public static bool IsReferenceOrNullableType(this Type type) + { + return (!type.IsValueType || Nullable.GetUnderlyingType(type) != null); + } + + public static object GetDefaultValue(this Type type) + { + return type.IsValueType + ? Activator.CreateInstance(type) + : null; + } + + public static Type[] GetInheritanceChain(this Type type) + { + var inheritanceChain = new List(); + + var current = type; + while (current.BaseType != null) + { + inheritanceChain.Add(current.BaseType); + current = current.BaseType; + } + + return inheritanceChain.ToArray(); + } + } +} diff --git a/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/ApiDescriptionExtensions.cs b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/ApiDescriptionExtensions.cs new file mode 100644 index 000000000..0220973ab --- /dev/null +++ b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/ApiDescriptionExtensions.cs @@ -0,0 +1,61 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Reflection; +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Routing.Template; + +namespace Surging.Core.Swagger_V5.SwaggerGen +{ + public static class ApiDescriptionExtensions + { + public static bool TryGetMethodInfo(this ApiDescription apiDescription, out MethodInfo methodInfo) + { + if (apiDescription.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor) + { + methodInfo = controllerActionDescriptor.MethodInfo; + return true; + } + + methodInfo = null; + return false; + } + + public static IEnumerable CustomAttributes(this ApiDescription apiDescription) + { + if (apiDescription.TryGetMethodInfo(out MethodInfo methodInfo)) + { + return methodInfo.GetCustomAttributes(true) + .Union(methodInfo.DeclaringType.GetCustomAttributes(true)); + } + + return Enumerable.Empty(); + } + + [Obsolete("Use TryGetMethodInfo() and CustomAttributes() instead")] + public static void GetAdditionalMetadata(this ApiDescription apiDescription, + out MethodInfo methodInfo, + out IEnumerable customAttributes) + { + if (apiDescription.TryGetMethodInfo(out methodInfo)) + { + customAttributes = methodInfo.GetCustomAttributes(true) + .Union(methodInfo.DeclaringType.GetCustomAttributes(true)); + + return; + } + + customAttributes = Enumerable.Empty(); + } + + internal static string RelativePathSansParameterConstraints(this ApiDescription apiDescription) + { + var routeTemplate = TemplateParser.Parse(apiDescription.RelativePath); + var sanitizedSegments = routeTemplate + .Segments + .Select(s => string.Concat(s.Parts.Select(p => p.Name != null ? $"{{{p.Name}}}" : p.Text))); + return string.Join("/", sanitizedSegments); + } + } +} diff --git a/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/ApiParameterDescriptionExtensions.cs b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/ApiParameterDescriptionExtensions.cs new file mode 100644 index 000000000..8d7f6c619 --- /dev/null +++ b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/ApiParameterDescriptionExtensions.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Reflection; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace Surging.Core.Swagger_V5.SwaggerGen +{ + public static class ApiParameterDescriptionExtensions + { + private static readonly Type[] RequiredAttributeTypes = new[] + { + typeof(BindRequiredAttribute), + typeof(RequiredAttribute) + }; + + public static bool IsRequiredParameter(this ApiParameterDescription apiParameter) + { + // From the OpenAPI spec: + // If the parameter location is "path", this property is REQUIRED and its value MUST be true. + if (apiParameter.IsFromPath()) + { + return true; + } + + // This is the default logic for IsRequired + bool IsRequired() => apiParameter.CustomAttributes().Any(attr => RequiredAttributeTypes.Contains(attr.GetType())); + + // This is to keep compatibility with MVC controller logic that has existed in the past + if (apiParameter.ParameterDescriptor is ControllerParameterDescriptor) + { + return IsRequired(); + } + + // For non-controllers, prefer the IsRequired flag if we're not on netstandard 2.0, otherwise fallback to the default logic. + return +#if !NETSTANDARD2_0 + apiParameter.IsRequired; +#else + IsRequired(); +#endif + } + + public static ParameterInfo ParameterInfo(this ApiParameterDescription apiParameter) + { + var parameterDescriptor = apiParameter.ParameterDescriptor as +#if NETCOREAPP2_2_OR_GREATER + Microsoft.AspNetCore.Mvc.Infrastructure.IParameterInfoParameterDescriptor; +#else + ControllerParameterDescriptor; +#endif + + return parameterDescriptor?.ParameterInfo; + } + + public static PropertyInfo PropertyInfo(this ApiParameterDescription apiParameter) + { + var modelMetadata = apiParameter.ModelMetadata; + + return (modelMetadata?.ContainerType != null) + ? modelMetadata.ContainerType.GetProperty(modelMetadata.PropertyName) + : null; + } + + public static IEnumerable CustomAttributes(this ApiParameterDescription apiParameter) + { + var propertyInfo = apiParameter.PropertyInfo(); + if (propertyInfo != null) return propertyInfo.GetCustomAttributes(true); + + var parameterInfo = apiParameter.ParameterInfo(); + if (parameterInfo != null) return parameterInfo.GetCustomAttributes(true); + + return Enumerable.Empty(); + } + + [Obsolete("Use ParameterInfo(), PropertyInfo() and CustomAttributes() extension methods instead")] + internal static void GetAdditionalMetadata( + this ApiParameterDescription apiParameter, + ApiDescription apiDescription, + out ParameterInfo parameterInfo, + out PropertyInfo propertyInfo, + out IEnumerable parameterOrPropertyAttributes) + { + parameterInfo = apiParameter.ParameterInfo(); + propertyInfo = apiParameter.PropertyInfo(); + parameterOrPropertyAttributes = apiParameter.CustomAttributes(); + } + + internal static bool IsFromPath(this ApiParameterDescription apiParameter) + { + return (apiParameter.Source == BindingSource.Path); + } + + internal static bool IsFromBody(this ApiParameterDescription apiParameter) + { + return (apiParameter.Source == BindingSource.Body); + } + + internal static bool IsFromForm(this ApiParameterDescription apiParameter) + { + var source = apiParameter.Source; + var elementType = apiParameter.ModelMetadata?.ElementType; + + return (source == BindingSource.Form || source == BindingSource.FormFile) + || (elementType != null && typeof(IFormFile).IsAssignableFrom(elementType)); + } + } +} \ No newline at end of file diff --git a/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/ApiResponseTypeExtensions.cs b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/ApiResponseTypeExtensions.cs new file mode 100644 index 000000000..13c45b123 --- /dev/null +++ b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/ApiResponseTypeExtensions.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Mvc.ApiExplorer; + +namespace Surging.Core.Swagger_V5.SwaggerGen +{ + public static class ApiResponseTypeExtensions + { + internal static bool IsDefaultResponse(this ApiResponseType apiResponseType) + { + var propertyInfo = apiResponseType.GetType().GetProperty("IsDefaultResponse"); + if (propertyInfo != null) + { + return (bool)propertyInfo.GetValue(apiResponseType); + } + + // ApiExplorer < 2.1.0 does not support default response. + return false; + } + } +} diff --git a/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/IDictionary.cs b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/IDictionary.cs new file mode 100644 index 000000000..ce7d81a0f --- /dev/null +++ b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/IDictionary.cs @@ -0,0 +1,6 @@ +namespace Surging.Core.Swagger_V5.SwaggerGen +{ + public interface IDictionary + { + } +} \ No newline at end of file diff --git a/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/IDocumentFilter.cs b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/IDocumentFilter.cs new file mode 100644 index 000000000..e8696b98e --- /dev/null +++ b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/IDocumentFilter.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.OpenApi.Models; + +namespace Surging.Core.Swagger_V5.SwaggerGen +{ + public interface IDocumentFilter + { + void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context); + } + + public class DocumentFilterContext + { + public DocumentFilterContext( + IEnumerable apiDescriptions, + ISchemaGenerator schemaGenerator, + SchemaRepository schemaRepository) + { + ApiDescriptions = apiDescriptions; + SchemaGenerator = schemaGenerator; + SchemaRepository = schemaRepository; + } + + public IEnumerable ApiDescriptions { get; } + + public ISchemaGenerator SchemaGenerator { get; } + + public SchemaRepository SchemaRepository { get; } + + public string DocumentName => SchemaRepository.DocumentName; + } +} diff --git a/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/IFileResult.cs b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/IFileResult.cs new file mode 100644 index 000000000..30d0168b0 --- /dev/null +++ b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/IFileResult.cs @@ -0,0 +1,6 @@ +namespace Surging.Core.Swagger_V5.SwaggerGen +{ + internal interface IFileResult + { + } +} \ No newline at end of file diff --git a/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/IOperationFilter.cs b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/IOperationFilter.cs new file mode 100644 index 000000000..a05a03010 --- /dev/null +++ b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/IOperationFilter.cs @@ -0,0 +1,48 @@ +using System.Reflection; +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.OpenApi.Models; +using Surging.Core.CPlatform.Runtime.Server; + +namespace Surging.Core.Swagger_V5.SwaggerGen +{ + public interface IOperationFilter + { + void Apply(OpenApiOperation operation, OperationFilterContext context); + } + + public class OperationFilterContext + { + public OperationFilterContext( + ApiDescription apiDescription, + ISchemaGenerator schemaRegistry, + SchemaRepository schemaRepository, + MethodInfo methodInfo):this(apiDescription, schemaRegistry, schemaRepository, methodInfo,null) + { + } + + public OperationFilterContext( + ApiDescription apiDescription, + ISchemaGenerator schemaRegistry, + SchemaRepository schemaRepository, + MethodInfo methodInfo,ServiceEntry serviceEntry) + { + ApiDescription = apiDescription; + SchemaGenerator = schemaRegistry; + SchemaRepository = schemaRepository; + MethodInfo = methodInfo; + ServiceEntry = serviceEntry; + } + + public ServiceEntry ServiceEntry { get; set; } + + public ApiDescription ApiDescription { get; } + + public ISchemaGenerator SchemaGenerator { get; } + + public SchemaRepository SchemaRepository { get; } + + public MethodInfo MethodInfo { get; } + + public string DocumentName => SchemaRepository.DocumentName; + } +} diff --git a/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/IParameterFilter.cs b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/IParameterFilter.cs new file mode 100644 index 000000000..b7950433d --- /dev/null +++ b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/IParameterFilter.cs @@ -0,0 +1,40 @@ +using System.Reflection; +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.OpenApi.Models; + +namespace Surging.Core.Swagger_V5.SwaggerGen +{ + public interface IParameterFilter + { + void Apply(OpenApiParameter parameter, ParameterFilterContext context); + } + + public class ParameterFilterContext + { + public ParameterFilterContext( + ApiParameterDescription apiParameterDescription, + ISchemaGenerator schemaGenerator, + SchemaRepository schemaRepository, + PropertyInfo propertyInfo = null, + ParameterInfo parameterInfo = null) + { + ApiParameterDescription = apiParameterDescription; + SchemaGenerator = schemaGenerator; + SchemaRepository = schemaRepository; + PropertyInfo = propertyInfo; + ParameterInfo = parameterInfo; + } + + public ApiParameterDescription ApiParameterDescription { get; } + + public ISchemaGenerator SchemaGenerator { get; } + + public SchemaRepository SchemaRepository { get; } + + public PropertyInfo PropertyInfo { get; } + + public ParameterInfo ParameterInfo { get; } + + public string DocumentName => SchemaRepository.DocumentName; + } +} diff --git a/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/IRequestBodyFilter.cs b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/IRequestBodyFilter.cs new file mode 100644 index 000000000..d4e0518b8 --- /dev/null +++ b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/IRequestBodyFilter.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.OpenApi.Models; + +namespace Surging.Core.Swagger_V5.SwaggerGen +{ + public interface IRequestBodyFilter + { + void Apply(OpenApiRequestBody requestBody, RequestBodyFilterContext context); + } + + public class RequestBodyFilterContext + { + public RequestBodyFilterContext( + ApiParameterDescription bodyParameterDescription, + IEnumerable formParameterDescriptions, + ISchemaGenerator schemaGenerator, + SchemaRepository schemaRepository) + { + BodyParameterDescription = bodyParameterDescription; + FormParameterDescriptions = formParameterDescriptions; + SchemaGenerator = schemaGenerator; + SchemaRepository = schemaRepository; + } + + public ApiParameterDescription BodyParameterDescription { get; } + + public IEnumerable FormParameterDescriptions { get; } + + public ISchemaGenerator SchemaGenerator { get; } + + public SchemaRepository SchemaRepository { get; } + + public string DocumentName => SchemaRepository.DocumentName; + } +} diff --git a/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/ISchemaGenerator.cs b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/ISchemaGenerator.cs new file mode 100644 index 000000000..943d16cb1 --- /dev/null +++ b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/ISchemaGenerator.cs @@ -0,0 +1,15 @@ +using System; +using System.Reflection; +using Microsoft.OpenApi.Models; + +namespace Surging.Core.Swagger_V5.SwaggerGen +{ + public interface ISchemaGenerator + { + OpenApiSchema GenerateSchema( + Type modelType, + SchemaRepository schemaRepository, + MemberInfo memberInfo = null, + ParameterInfo parameterInfo = null); + } +} diff --git a/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/OpenApiAnyFactory.cs b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/OpenApiAnyFactory.cs new file mode 100644 index 000000000..09b046ded --- /dev/null +++ b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/OpenApiAnyFactory.cs @@ -0,0 +1,80 @@ +using System.Text.Json; +using Microsoft.OpenApi.Any; + +namespace Surging.Core.Swagger_V5.SwaggerGen +{ + public static class OpenApiAnyFactory + { + public static IOpenApiAny CreateFromJson(string json) + { + try + { + var jsonElement = JsonSerializer.Deserialize(json); + + return CreateFromJsonElement(jsonElement); + } + catch { } + + return null; + } + + private static IOpenApiAny CreateOpenApiArray(JsonElement jsonElement) + { + var openApiArray = new OpenApiArray(); + + foreach (var item in jsonElement.EnumerateArray()) + { + openApiArray.Add(CreateFromJsonElement(item)); + } + + return openApiArray; + } + + private static IOpenApiAny CreateOpenApiObject(JsonElement jsonElement) + { + var openApiObject = new OpenApiObject(); + + foreach (var property in jsonElement.EnumerateObject()) + { + openApiObject.Add(property.Name, CreateFromJsonElement(property.Value)); + } + + return openApiObject; + } + + private static IOpenApiAny CreateFromJsonElement(JsonElement jsonElement) + { + if (jsonElement.ValueKind == JsonValueKind.Null) + return new OpenApiNull(); + + if (jsonElement.ValueKind == JsonValueKind.True || jsonElement.ValueKind == JsonValueKind.False) + return new OpenApiBoolean(jsonElement.GetBoolean()); + + if (jsonElement.ValueKind == JsonValueKind.Number) + { + if (jsonElement.TryGetInt32(out int intValue)) + return new OpenApiInteger(intValue); + + if (jsonElement.TryGetInt64(out long longValue)) + return new OpenApiLong(longValue); + + if (jsonElement.TryGetSingle(out float floatValue) && !float.IsInfinity(floatValue)) + return new OpenApiFloat(floatValue); + + if (jsonElement.TryGetDouble(out double doubleValue)) + return new OpenApiDouble(doubleValue); + } + + if (jsonElement.ValueKind == JsonValueKind.String) + return new OpenApiString(jsonElement.ToString()); + + if (jsonElement.ValueKind == JsonValueKind.Array) + return CreateOpenApiArray(jsonElement); + + if (jsonElement.ValueKind == JsonValueKind.Object) + return CreateOpenApiObject(jsonElement); + + throw new System.ArgumentException($"Unsupported value kind {jsonElement.ValueKind}"); + } + } +} diff --git a/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/SchemaRepository.cs b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/SchemaRepository.cs new file mode 100644 index 000000000..8130fa91c --- /dev/null +++ b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/SchemaRepository.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Models; + +namespace Surging.Core.Swagger_V5.SwaggerGen +{ + public class SchemaRepository + { + private readonly Dictionary _reservedIds = new Dictionary(); + + public SchemaRepository(string documentName = null) + { + DocumentName = documentName; + } + + public string DocumentName { get; } + + public Dictionary Schemas { get; private set; } = new Dictionary(); + + public void RegisterType(Type type, string schemaId) + { + if (_reservedIds.ContainsValue(schemaId)) + { + var conflictingType = _reservedIds.First(entry => entry.Value == schemaId).Key; + + throw new InvalidOperationException( + $"Can't use schemaId \"${schemaId}\" for type \"${type}\". " + + $"The same schemaId is already used for type \"${conflictingType}\""); + } + + _reservedIds.Add(type, schemaId); + } + + public bool TryLookupByType(Type type, out OpenApiSchema referenceSchema) + { + if (_reservedIds.TryGetValue(type, out string schemaId)) + { + referenceSchema = new OpenApiSchema + { + Reference = new OpenApiReference { Type = ReferenceType.Schema, Id = schemaId } + }; + return true; + } + + referenceSchema = null; + return false; + } + + public OpenApiSchema AddDefinition(string schemaId, OpenApiSchema schema) + { + Schemas.Add(schemaId, schema); + + return new OpenApiSchema + { + Reference = new OpenApiReference { Type = ReferenceType.Schema, Id = schemaId } + }; + } + } +} diff --git a/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/StringExtensions.cs b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/StringExtensions.cs new file mode 100644 index 000000000..2d557d062 --- /dev/null +++ b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/StringExtensions.cs @@ -0,0 +1,17 @@ +using System.Linq; + +namespace Surging.Core.Swagger_V5.SwaggerGen +{ + internal static class StringExtensions + { + internal static string ToCamelCase(this string value) + { + if (string.IsNullOrEmpty(value)) return value; + + var cameCasedParts = value.Split('.') + .Select(part => char.ToLowerInvariant(part[0]) + part.Substring(1)); + + return string.Join(".", cameCasedParts); + } + } +} diff --git a/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs new file mode 100644 index 000000000..f0434acc0 --- /dev/null +++ b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs @@ -0,0 +1,925 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Reflection.Metadata; +using System.Text.RegularExpressions; +using Autofac; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.Extensions.Primitives; +using Microsoft.OpenApi.Models; +using Surging.Core.CPlatform.Messages; +using Surging.Core.CPlatform.Runtime.Server; +using Surging.Core.CPlatform.Runtime.Server.Implementation.ServiceDiscovery.Attributes; +using Surging.Core.CPlatform.Utilities; +using Surging.Core.Swagger_V5; +using Surging.Core.Swagger_V5.Swagger; + +namespace Surging.Core.Swagger_V5.SwaggerGen +{ + public class SwaggerGenerator : ISwaggerProvider + { + private readonly IApiDescriptionGroupCollectionProvider _apiDescriptionsProvider; + private readonly ISchemaGenerator _schemaGenerator; + private readonly SwaggerGeneratorOptions _options; + private readonly IServiceEntryProvider _serviceEntryProvider; + + public SwaggerGenerator( + SwaggerGeneratorOptions options, + IApiDescriptionGroupCollectionProvider apiDescriptionsProvider, + ISchemaGenerator schemaGenerator) + { + _options = options ?? new SwaggerGeneratorOptions(); + _apiDescriptionsProvider = apiDescriptionsProvider; + _schemaGenerator = schemaGenerator; + _serviceEntryProvider = ServiceLocator.Current.Resolve(); + } + + public OpenApiDocument GetSwagger(string documentName, string host = null, string basePath = null) + { + if (!_options.SwaggerDocs.TryGetValue(documentName, out OpenApiInfo info)) + throw new UnknownSwaggerDocument(documentName, _options.SwaggerDocs.Select(d => d.Key)); + + var applicableApiDescriptions = _apiDescriptionsProvider.ApiDescriptionGroups.Items + .SelectMany(group => group.Items) + .Where(apiDesc => !(_options.IgnoreObsoleteActions && apiDesc.CustomAttributes().OfType().Any())) + .Where(apiDesc => _options.DocInclusionPredicate(documentName, apiDesc)); + + var schemaRepository = new SchemaRepository(documentName); + var entries = _serviceEntryProvider.GetALLEntries(); + var mapRoutePaths = AppConfig.SwaggerConfig.Options?.MapRoutePaths; + if (mapRoutePaths != null) + { + foreach (var path in mapRoutePaths) + { + var entry = entries.Where(p => p.RoutePath == path.SourceRoutePath).FirstOrDefault(); + if (entry != null) + { + entry.RoutePath = path.TargetRoutePath; + entry.Descriptor.RoutePath = path.TargetRoutePath; + } + } + } + entries = entries + .Where(apiDesc => _options.DocInclusionPredicateV2(documentName, apiDesc)); + try + { + + var swaggerDoc = new OpenApiDocument + { + Info = info, + Servers = GenerateServers(host, basePath), + Paths = GeneratePaths(entries, schemaRepository), + Components = new OpenApiComponents + { + Schemas = schemaRepository.Schemas, + SecuritySchemes = new Dictionary(_options.SecuritySchemes) + }, + SecurityRequirements = new List(_options.SecurityRequirements) + }; + + var filterContext = new DocumentFilterContext(applicableApiDescriptions, _schemaGenerator, schemaRepository); + foreach (var filter in _options.DocumentFilters) + { + filter.Apply(swaggerDoc, filterContext); + } + + swaggerDoc.Components.Schemas = new SortedDictionary(swaggerDoc.Components.Schemas, _options.SchemaComparer); + + return swaggerDoc; + } + catch(Exception ex) + { + throw ex; + } + } + + private IList GenerateServers(string host, string basePath) + { + if (_options.Servers.Any()) + { + return new List(_options.Servers); + } + + return (host == null && basePath == null) + ? new List() + : new List { new OpenApiServer { Url = $"{host}{basePath}" } }; + } + + private OpenApiPaths GeneratePaths(IEnumerable apiDescriptions, SchemaRepository schemaRepository) + { + var apiDescriptionsByPath = apiDescriptions + .OrderBy(_options.SortKeySelector) + .GroupBy(apiDesc => apiDesc.RelativePathSansParameterConstraints()); + + var paths = new OpenApiPaths(); + foreach (var group in apiDescriptionsByPath) + { + paths.Add($"/{group.Key}", + new OpenApiPathItem + { + Operations = GenerateOperations(group, schemaRepository) + }); + }; + + return paths; + } + + private OpenApiPaths GeneratePaths(IEnumerable apiDescriptions, SchemaRepository schemaRepository) + { + var apiDescriptionsByPath = apiDescriptions.OrderBy(p => p.RoutePath) + .GroupBy(apiDesc => apiDesc.Descriptor.Id); + + var paths = new OpenApiPaths(); + foreach (var group in apiDescriptionsByPath) + { + var key = $"/{group.Min(p => p.RoutePath).TrimStart('/')}".Trim(); + if(!paths.ContainsKey(key)) + paths.Add(key, + new OpenApiPathItem + { + Operations = GenerateOperations(group, schemaRepository) + }); + else + paths.Add($"/{group.Min(p => $" {p.RoutePath}( {string.Join("_", p.Parameters.Select(m => m.ParameterType.Name))})")}", + new OpenApiPathItem + { + Operations = GenerateOperations(group, schemaRepository) + }); + + }; + + return paths; + } + + private IDictionary GenerateOperations( + IEnumerable apiDescriptions, + SchemaRepository schemaRepository) + { + var apiDescriptionsByMethod = apiDescriptions + .OrderBy(_options.SortKeySelector) + .GroupBy(apiDesc => apiDesc.HttpMethod); + + var operations = new Dictionary(); + + foreach (var group in apiDescriptionsByMethod) + { + var httpMethod = group.Key; + + if (httpMethod == null) + throw new SwaggerGeneratorException(string.Format( + "Ambiguous HTTP method for action - {0}. " + + "Actions require an explicit HttpMethod binding for Swagger/OpenAPI 3.0", + group.First().ActionDescriptor.DisplayName)); + + if (group.Count() > 1 && _options.ConflictingActionsResolver == null) + throw new SwaggerGeneratorException(string.Format( + "Conflicting method/path combination \"{0} {1}\" for actions - {2}. " + + "Actions require a unique method/path combination for Swagger/OpenAPI 3.0. Use ConflictingActionsResolver as a workaround", + httpMethod, + group.First().RelativePath, + string.Join(",", group.Select(apiDesc => apiDesc.ActionDescriptor.DisplayName)))); + + var apiDescription = (group.Count() > 1) ? _options.ConflictingActionsResolver(group) : group.Single(); + + operations.Add(OperationTypeMap[httpMethod.ToUpper()], GenerateOperation(apiDescription, schemaRepository)); + }; + + return operations; + } + + private IDictionary GenerateOperations( + IEnumerable apiDescriptions, + SchemaRepository schemaRepository) + { + + var operations = new Dictionary(); + + foreach (var entry in apiDescriptions) + { + try + { + var httpMethod = ""; + var methodInfo = entry.Type.GetTypeInfo().DeclaredMethods.Where(p => p.Name == entry.MethodName).FirstOrDefault(); + var parameterInfo = methodInfo.GetParameters(); + + if (entry.Methods.Count() == 0) + { + if (parameterInfo != null && parameterInfo.Any(p => + !UtilityType.ConvertibleType.GetTypeInfo().IsAssignableFrom(p.ParameterType))) + httpMethod = "post"; + else + httpMethod = "get"; + operations.Add(OperationTypeMap[httpMethod.ToUpper()], GenerateOperation(entry, schemaRepository)); + } + else if(entry.Methods.Count() >0) + { + foreach(var method in entry.Methods) + operations.Add(OperationTypeMap[method.ToUpper()], GenerateOperation(entry, schemaRepository)); + } + //var apiDescription = (group.Count() > 1) ? _options.ConflictingActionsResolver(group) : group.Single(); + + + } + catch(Exception ex) + { + throw ex; + } + }; + + return operations; + } + + private OpenApiOperation GenerateOperation(ServiceEntry entry, SchemaRepository schemaRepository) + { + try + { + var operation = new OpenApiOperation + { + Tags = GenerateOperationTags(entry), + OperationId = _options.EntryOperationIdSelector(entry), + Parameters = GenerateParameters(entry, schemaRepository), + RequestBody = GenerateRequestBody(entry, schemaRepository), + Responses = GenerateResponses(entry, schemaRepository), + Deprecated = entry.Attributes.OfType().Any() + }; + + var methodInfo = entry.Type.GetTypeInfo().DeclaredMethods.Where(p => p.Name == entry.MethodName).FirstOrDefault(); + var filterContext = new OperationFilterContext(null, _schemaGenerator, schemaRepository, methodInfo,entry); + foreach (var filter in _options.OperationFilters) + { + filter.Apply(operation, filterContext); + } + + return operation; + } + catch (Exception ex) + { + throw new SwaggerGeneratorException( + message: $"Failed to generate Operation for action - {entry.RoutePath}. See inner exception", + innerException: ex); + } + } + + private OpenApiOperation GenerateOperation(ApiDescription apiDescription, SchemaRepository schemaRepository) + { + try + { + var operation = new OpenApiOperation + { + Tags = GenerateOperationTags(apiDescription), + OperationId = _options.OperationIdSelector(apiDescription), + Parameters = GenerateParameters(apiDescription, schemaRepository), + RequestBody = GenerateRequestBody(apiDescription, schemaRepository), + Responses = GenerateResponses(apiDescription, schemaRepository), + Deprecated = apiDescription.CustomAttributes().OfType().Any() + }; + + apiDescription.TryGetMethodInfo(out MethodInfo methodInfo); + var filterContext = new OperationFilterContext(apiDescription, _schemaGenerator, schemaRepository, methodInfo); + foreach (var filter in _options.OperationFilters) + { + filter.Apply(operation, filterContext); + } + + return operation; + } + catch (Exception ex) + { + throw new SwaggerGeneratorException( + message: $"Failed to generate Operation for action - {apiDescription.ActionDescriptor.DisplayName}. See inner exception", + innerException: ex); + } + } + + private IList GenerateOperationTags(ServiceEntry apiDescription) + { + return _options.EntryTagsSelector(apiDescription) + .Select(tagName => new OpenApiTag { Name = tagName }) + .ToList(); + } + + private IList GenerateOperationTags(ApiDescription apiDescription) + { + return _options.TagsSelector(apiDescription) + .Select(tagName => new OpenApiTag { Name = tagName }) + .ToList(); + } + + private IList GenerateParameters(ApiDescription apiDescription, SchemaRepository schemaRespository) + { + var applicableApiParameters = apiDescription.ParameterDescriptions + .Where(apiParam => + { + return (!apiParam.IsFromBody() && !apiParam.IsFromForm()) + && (!apiParam.CustomAttributes().OfType().Any()) + && (apiParam.ModelMetadata == null || apiParam.ModelMetadata.IsBindingAllowed); + }); + + return applicableApiParameters + .Select(apiParam => GenerateParameter(apiParam, schemaRespository)) + .ToList(); + } + + private IList GenerateParameters(ServiceEntry apiDescription, SchemaRepository schemaRespository) + { + var applicableApiParameters = apiDescription.Parameters + .Where(apiParam => + { + return apiParam != null && + UtilityType.ConvertibleType.GetTypeInfo().IsAssignableFrom(apiParam?.ParameterType); + }); + + return applicableApiParameters + .Select(apiParam => GenerateParameter(apiParam, schemaRespository)) + .Union(new OpenApiParameter[] { CreateServiceKeyParameter(schemaRespository) }).ToList(); + } + + private OpenApiParameter GenerateParameter( + ApiParameterDescription apiParameter, + SchemaRepository schemaRepository) + { + var name = _options.DescribeAllParametersInCamelCase + ? apiParameter.Name.ToCamelCase() + : apiParameter.Name; + + var location = (apiParameter.Source != null && ParameterLocationMap.ContainsKey(apiParameter.Source)) + ? ParameterLocationMap[apiParameter.Source] + : ParameterLocation.Query; + + var isRequired = apiParameter.IsRequiredParameter(); + + var schema = (apiParameter.ModelMetadata != null) + ? GenerateSchema( + apiParameter.ModelMetadata.ModelType, + schemaRepository, + apiParameter.PropertyInfo(), + apiParameter.ParameterInfo()) + : new OpenApiSchema { Type = "string" }; + + var parameter = new OpenApiParameter + { + Name = name, + In = location, + Required = isRequired, + Schema = schema + }; + + var filterContext = new ParameterFilterContext( + apiParameter, + _schemaGenerator, + schemaRepository, + apiParameter.PropertyInfo(), + apiParameter.ParameterInfo()); + + foreach (var filter in _options.ParameterFilters) + { + filter.Apply(parameter, filterContext); + } + + return parameter; + } + + private OpenApiParameter GenerateParameter( + ParameterInfo apiParameter, + SchemaRepository schemaRepository) + { + var name = _options.DescribeAllParametersInCamelCase + ? apiParameter.Name?.ToCamelCase() + : apiParameter.Name; + + var location = ParameterLocation.Query; + + var isRequired = true; + + var schema = GenerateSchema( + apiParameter.ParameterType, + schemaRepository, + default, + apiParameter); + + + var parameter = new OpenApiParameter + { + Name = name, + In = location, + Required = isRequired, + Schema = schema + }; + + var filterContext = new ParameterFilterContext( + null, + _schemaGenerator, + schemaRepository, + default, + apiParameter); + + foreach (var filter in _options.ParameterFilters) + { + filter.Apply(parameter, filterContext); + } + return parameter; + } + + private OpenApiParameter CreateServiceKeyParameter(SchemaRepository schemaRepository) + { + var schema = GenerateSchema( + typeof(string), + schemaRepository); + + schema.Description = "ServiceKey"; + + var parameter = new OpenApiParameter + { + Name = "ServiceKey", + In = ParameterLocation.Query, + Required = false, + Schema = schema + }; + return parameter; + } + + private OpenApiSchema GenerateSchema( + Type type, + SchemaRepository schemaRepository, + PropertyInfo propertyInfo = null, + ParameterInfo parameterInfo = null) + { + try + { + return _schemaGenerator.GenerateSchema(type, schemaRepository, propertyInfo, parameterInfo); + } + catch (Exception ex) + { + throw new SwaggerGeneratorException( + message: $"Failed to generate schema for type - {type}. See inner exception", + innerException: ex); + } + } + + + + private OpenApiRequestBody GenerateRequestBody( + ApiDescription apiDescription, + SchemaRepository schemaRepository) + { + OpenApiRequestBody requestBody = null; + RequestBodyFilterContext filterContext = null; + + var bodyParameter = apiDescription.ParameterDescriptions + .FirstOrDefault(paramDesc => paramDesc.IsFromBody()); + + var formParameters = apiDescription.ParameterDescriptions + .Where(paramDesc => paramDesc.IsFromForm()); + + if (bodyParameter != null) + { + requestBody = GenerateRequestBodyFromBodyParameter(apiDescription, schemaRepository, bodyParameter); + + filterContext = new RequestBodyFilterContext( + bodyParameterDescription: bodyParameter, + formParameterDescriptions: null, + schemaGenerator: _schemaGenerator, + schemaRepository: schemaRepository); + } + else if (formParameters.Any()) + { + requestBody = GenerateRequestBodyFromFormParameters(apiDescription, schemaRepository, formParameters); + + filterContext = new RequestBodyFilterContext( + bodyParameterDescription: null, + formParameterDescriptions: formParameters, + schemaGenerator: _schemaGenerator, + schemaRepository: schemaRepository); + } + + if (requestBody != null) + { + foreach (var filter in _options.RequestBodyFilters) + { + filter.Apply(requestBody, filterContext); + } + } + + return requestBody; + } + + private OpenApiRequestBody GenerateRequestBody( + ServiceEntry entry, + SchemaRepository schemaRepository) + { + OpenApiRequestBody requestBody = null; + RequestBodyFilterContext filterContext = null; + + var parameters = entry.Parameters; + + + if (parameters != null && parameters.Any(p => + !UtilityType.ConvertibleType.GetTypeInfo().IsAssignableFrom(p.ParameterType) && p.ParameterType.Name != "HttpFormCollection")) + { + + requestBody = GenerateRequestBodyFromBodyParameter(entry, schemaRepository, parameters); + filterContext = new RequestBodyFilterContext( + bodyParameterDescription: null, + formParameterDescriptions: null, + schemaGenerator: _schemaGenerator, + schemaRepository: schemaRepository); + + } + else if (parameters != null && parameters.Any(p => + !UtilityType.ConvertibleType.GetTypeInfo().IsAssignableFrom(p.ParameterType) && p.ParameterType.Name == "HttpFormCollection")) + { + requestBody = GenerateRequestBodyFromFormParameters(entry, schemaRepository, parameters); + filterContext = new RequestBodyFilterContext( + bodyParameterDescription: null, + formParameterDescriptions: null, + schemaGenerator: _schemaGenerator, + schemaRepository: schemaRepository); + } + + if (requestBody != null) + { + foreach (var filter in _options.RequestBodyFilters) + { + filter.Apply(requestBody, filterContext); + } + } + + return requestBody; + } + + private OpenApiRequestBody GenerateRequestBodyFromBodyParameter( + ApiDescription apiDescription, + SchemaRepository schemaRepository, + ApiParameterDescription bodyParameter) + { + var contentTypes = InferRequestContentTypes(apiDescription); + + var isRequired = bodyParameter.IsRequiredParameter(); + + var schema = GenerateSchema( + bodyParameter.ModelMetadata.ModelType, + schemaRepository, + bodyParameter.PropertyInfo(), + bodyParameter.ParameterInfo()); + + return new OpenApiRequestBody + { + Content = contentTypes + .ToDictionary( + contentType => contentType, + contentType => new OpenApiMediaType + { + Schema = schema + } + ), + Required = isRequired + }; + } + + private OpenApiRequestBody GenerateRequestBodyFromBodyParameter( + ServiceEntry entry, + SchemaRepository schemaRepository, + ParameterInfo[] bodyParameter) + { + var contentTypes =new string[] { "application/json"}; + + var isRequired = true; + var properties = new Dictionary(); + var requiredPropertyNames = new List(); + + foreach (var formParameter in bodyParameter) + { + var name = _options.DescribeAllParametersInCamelCase + ? formParameter.Name.ToCamelCase() + : formParameter.Name; + + var propertySchema = new OpenApiSchema { Type = "string" }; + + if (formParameter.ParameterType != null) + { + + propertySchema = GenerateSchema( + formParameter.ParameterType, + schemaRepository, + default, + formParameter); + } + + properties.Add(name, propertySchema); + + requiredPropertyNames.Add(name); + } + + var schema = new OpenApiSchema + { + Type = "object", + Properties = properties, + Required = new SortedSet(requiredPropertyNames) + }; + return new OpenApiRequestBody + { + Content = contentTypes + .ToDictionary( + contentType => contentType, + contentType => new OpenApiMediaType + { + Schema = schema, + Encoding = schema.Properties.ToDictionary( + entry => entry.Key, + entry => new OpenApiEncoding { Style = ParameterStyle.DeepObject } + ) + } + ), + Required = isRequired + }; + } + + private IEnumerable InferRequestContentTypes(ApiDescription apiDescription) + { + // If there's content types explicitly specified via ConsumesAttribute, use them + var explicitContentTypes = apiDescription.CustomAttributes().OfType() + .SelectMany(attr => attr.ContentTypes) + .Distinct(); + if (explicitContentTypes.Any()) return explicitContentTypes; + + // If there's content types surfaced by ApiExplorer, use them + var apiExplorerContentTypes = apiDescription.SupportedRequestFormats + .Select(format => format.MediaType) + .Where(x => x != null) + .Distinct(); + if (apiExplorerContentTypes.Any()) return apiExplorerContentTypes; + + return Enumerable.Empty(); + } + + private OpenApiRequestBody GenerateRequestBodyFromFormParameters( +ServiceEntry entry, +SchemaRepository schemaRepository, +ParameterInfo[] bodyParameter) + { + var contentTypes = new string[] { "multipart/form-data" }; + + var isRequired = true; + var properties = new Dictionary(); + var requiredPropertyNames = new List(); + + foreach (var formParameter in bodyParameter) + { + var name = _options.DescribeAllParametersInCamelCase + ? formParameter.Name.ToCamelCase() + : formParameter.Name; + + var propertySchema = new OpenApiSchema { Type = "string" }; + if (typeof(IEnumerable>).IsAssignableFrom(formParameter.ParameterType) && + formParameter.ParameterType.Name == "HttpFormCollection") + { + propertySchema = new OpenApiSchema { Type = "file" }; + } + else if (formParameter.ParameterType != null) + { + + propertySchema = GenerateSchema( + formParameter.ParameterType, + schemaRepository, + default, + formParameter); + } + + properties.Add(name, propertySchema); + + requiredPropertyNames.Add(name); + } + var schema = new OpenApiSchema + { + Type = "object", + Properties = properties, + Required = new SortedSet(requiredPropertyNames) + }; + + + return new OpenApiRequestBody + { + Content = contentTypes + .ToDictionary( + contentType => contentType, + contentType => new OpenApiMediaType + { + Schema = schema, + Encoding = schema.Properties.ToDictionary( + entry => entry.Key, + entry => new OpenApiEncoding { Style = ParameterStyle.Form } + ) + } + ), + Required = isRequired + }; + } + + + private OpenApiRequestBody GenerateRequestBodyFromFormParameters( + ApiDescription apiDescription, + SchemaRepository schemaRepository, + IEnumerable formParameters) + { + var contentTypes = InferRequestContentTypes(apiDescription); + contentTypes = contentTypes.Any() ? contentTypes : new[] { "multipart/form-data" }; + + var schema = GenerateSchemaFromFormParameters(formParameters, schemaRepository); + + return new OpenApiRequestBody + { + Content = contentTypes + .ToDictionary( + contentType => contentType, + contentType => new OpenApiMediaType + { + Schema = schema, + Encoding = schema.Properties.ToDictionary( + entry => entry.Key, + entry => new OpenApiEncoding { Style = ParameterStyle.Form } + ) + } + ) + }; + } + + private OpenApiSchema GenerateSchemaFromFormParameters( + IEnumerable formParameters, + SchemaRepository schemaRepository) + { + var properties = new Dictionary(); + var requiredPropertyNames = new List(); + + foreach (var formParameter in formParameters) + { + var name = _options.DescribeAllParametersInCamelCase + ? formParameter.Name.ToCamelCase() + : formParameter.Name; + + var schema = (formParameter.ModelMetadata != null) + ? GenerateSchema( + formParameter.ModelMetadata.ModelType, + schemaRepository, + formParameter.PropertyInfo(), + formParameter.ParameterInfo()) + : new OpenApiSchema { Type = "string" }; + + properties.Add(name, schema); + + if (formParameter.IsRequiredParameter()) + requiredPropertyNames.Add(name); + } + + return new OpenApiSchema + { + Type = "object", + Properties = properties, + Required = new SortedSet(requiredPropertyNames) + }; + } + + private OpenApiResponses GenerateResponses( + ApiDescription apiDescription, + SchemaRepository schemaRepository) + { + var supportedResponseTypes = apiDescription.SupportedResponseTypes + .DefaultIfEmpty(new ApiResponseType { StatusCode = 200 }); + + var responses = new OpenApiResponses(); + foreach (var responseType in supportedResponseTypes) + { + var statusCode = responseType.IsDefaultResponse() ? "default" : responseType.StatusCode.ToString(); + responses.Add(statusCode, GenerateResponse(apiDescription, schemaRepository, statusCode, responseType)); + } + return responses; + } + + private OpenApiResponses GenerateResponses( + ServiceEntry entry, + SchemaRepository schemaRepository) + { + var description = ResponseDescriptionMap + .FirstOrDefault((entry) => Regex.IsMatch("200", entry.Key)) + .Value; + var methodInfo = entry.Type.GetTypeInfo().DeclaredMethods.Where(p => p.Name == entry.MethodName).FirstOrDefault(); + var responses = new OpenApiResponses(); + var content = new Dictionary(); + if (methodInfo.ReturnType != typeof(Task) && methodInfo.ReturnType != typeof(void)) + { + content.TryAdd("application/json", new OpenApiMediaType + { + Schema = GenerateSchema(typeof(HttpResultMessage<>).MakeGenericType(methodInfo.ReturnType.GenericTypeArguments), schemaRepository) + }); + + } + else + { + + content.TryAdd("application/json",new OpenApiMediaType + { + Schema =null + }); + + } + + responses.Add("200", new OpenApiResponse + { + Description = description, + Content = content + + }); + return responses; + } + + private OpenApiResponse GenerateResponse( + ApiDescription apiDescription, + SchemaRepository schemaRepository, + string statusCode, + ApiResponseType apiResponseType) + { + var description = ResponseDescriptionMap + .FirstOrDefault((entry) => Regex.IsMatch(statusCode, entry.Key)) + .Value; + + var responseContentTypes = InferResponseContentTypes(apiDescription, apiResponseType); + + return new OpenApiResponse + { + Description = description, + Content = responseContentTypes.ToDictionary( + contentType => contentType, + contentType => CreateResponseMediaType(apiResponseType.ModelMetadata, schemaRepository) + ) + }; + } + + private IEnumerable InferResponseContentTypes(ApiDescription apiDescription, ApiResponseType apiResponseType) + { + // If there's no associated model, return an empty list (i.e. no content) + if (apiResponseType.ModelMetadata == null) return Enumerable.Empty(); + + // If there's content types explicitly specified via ProducesAttribute, use them + var explicitContentTypes = apiDescription.CustomAttributes().OfType() + .SelectMany(attr => attr.ContentTypes) + .Distinct(); + if (explicitContentTypes.Any()) return explicitContentTypes; + + // If there's content types surfaced by ApiExplorer, use them + var apiExplorerContentTypes = apiResponseType.ApiResponseFormats + .Select(responseFormat => responseFormat.MediaType) + .Distinct(); + if (apiExplorerContentTypes.Any()) return apiExplorerContentTypes; + + return Enumerable.Empty(); + } + + private OpenApiMediaType CreateResponseMediaType(ModelMetadata modelMetadata, SchemaRepository schemaRespository) + { + return new OpenApiMediaType + { + Schema = GenerateSchema(modelMetadata.ModelType, schemaRespository) + }; + } + + private static readonly Dictionary OperationTypeMap = new Dictionary + { + { "GET", OperationType.Get }, + { "PUT", OperationType.Put }, + { "POST", OperationType.Post }, + { "DELETE", OperationType.Delete }, + { "OPTIONS", OperationType.Options }, + { "HEAD", OperationType.Head }, + { "PATCH", OperationType.Patch }, + { "TRACE", OperationType.Trace } + }; + + private static readonly Dictionary ParameterLocationMap = new Dictionary + { + { BindingSource.Query, ParameterLocation.Query }, + { BindingSource.Header, ParameterLocation.Header }, + { BindingSource.Path, ParameterLocation.Path } + }; + + private static readonly Dictionary ResponseDescriptionMap = new Dictionary + { + { "1\\d{2}", "Information" }, + { "2\\d{2}", "Success" }, + { "304", "Not Modified" }, + { "3\\d{2}", "Redirect" }, + { "400", "Bad Request" }, + { "401", "Unauthorized" }, + { "403", "Forbidden" }, + { "404", "Not Found" }, + { "405", "Method Not Allowed" }, + { "406", "Not Acceptable" }, + { "408", "Request Timeout" }, + { "409", "Conflict" }, + { "4\\d{2}", "Client Error" }, + { "5\\d{2}", "Server Error" }, + { "default", "Error" } + }; + } +} diff --git a/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/SwaggerGeneratorException.cs b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/SwaggerGeneratorException.cs new file mode 100644 index 000000000..1e7add6e0 --- /dev/null +++ b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/SwaggerGeneratorException.cs @@ -0,0 +1,13 @@ +using System; + +namespace Surging.Core.Swagger_V5.SwaggerGen +{ + public class SwaggerGeneratorException : Exception + { + public SwaggerGeneratorException(string message) : base(message) + { } + + public SwaggerGeneratorException(string message, Exception innerException) : base(message, innerException) + { } + } +} \ No newline at end of file diff --git a/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/SwaggerGeneratorOptions.cs b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/SwaggerGeneratorOptions.cs new file mode 100644 index 000000000..2e9791e3a --- /dev/null +++ b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/SwaggerGenerator/SwaggerGeneratorOptions.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.Linq; +#if NET6_0_OR_GREATER +using Microsoft.AspNetCore.Http.Metadata; +#endif +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.OpenApi.Models; +using Microsoft.AspNetCore.Routing; +using Surging.Core.CPlatform.Runtime.Server; +using System.Reflection; + +namespace Surging.Core.Swagger_V5.SwaggerGen +{ + public class SwaggerGeneratorOptions + { + public SwaggerGeneratorOptions() + { + SwaggerDocs = new Dictionary(); + DocInclusionPredicate = DefaultDocInclusionPredicate; + OperationIdSelector = DefaultOperationIdSelector; + EntryOperationIdSelector = DefaultEntryOperationIdSelector; + DocInclusionPredicateV2 = DefaultDocInclusionPredicateV2; + EntryTagsSelector = DefaultEntryTagsSelector; + TagsSelector = DefaultTagsSelector; + SortKeySelector = DefaultSortKeySelector; + SchemaComparer = StringComparer.Ordinal; + Servers = new List(); + SecuritySchemes = new Dictionary(); + SecurityRequirements = new List(); + ParameterFilters = new List(); + RequestBodyFilters = new List(); + OperationFilters = new List(); + DocumentFilters = new List(); + } + + public IDictionary SwaggerDocs { get; set; } + + public Func DocInclusionPredicate { get; set; } + + public bool IgnoreObsoleteActions { get; set; } + + public Func DocInclusionPredicateV2 { get; set; } + + public Func, ApiDescription> ConflictingActionsResolver { get; set; } + + public Func OperationIdSelector { get; set; } + + public Func EntryOperationIdSelector { get; set; } + + public Func> TagsSelector { get; set; } + + public Func> EntryTagsSelector { get; set; } + + public Func SortKeySelector { get; set; } + + public bool DescribeAllParametersInCamelCase { get; set; } + + public List Servers { get; set; } + + public IDictionary SecuritySchemes { get; set; } + + public IList SecurityRequirements { get; set; } + + public IComparer SchemaComparer { get; set; } + + public IList ParameterFilters { get; set; } + + public List RequestBodyFilters { get; set; } + + public List OperationFilters { get; set; } + + public IList DocumentFilters { get; set; } + + private bool DefaultDocInclusionPredicate(string documentName, ApiDescription apiDescription) + { + return apiDescription.GroupName == null || apiDescription.GroupName == documentName; + } + + private bool DefaultDocInclusionPredicateV2(string documentName, ServiceEntry apiDescription) + { + var assembly = apiDescription.Type.Assembly; + + var versions = assembly + .GetCustomAttributes(true) + .OfType(); + return versions != null; + } + + private string DefaultOperationIdSelector(ApiDescription apiDescription) + { + var actionDescriptor = apiDescription.ActionDescriptor; + + // Resolve the operation ID from the route name and fallback to the + // endpoint name if no route name is available. This allows us to + // generate operation IDs for endpoints that are defined using + // minimal APIs. +#if (!NETSTANDARD2_0) + return + actionDescriptor.AttributeRouteInfo?.Name + ?? (actionDescriptor.EndpointMetadata?.LastOrDefault(m => m is IEndpointNameMetadata) as IEndpointNameMetadata)?.EndpointName; +#else + return actionDescriptor.AttributeRouteInfo?.Name; +#endif + } + + private string DefaultEntryOperationIdSelector(ServiceEntry entry) + { + return entry.RoutePath; + } + + private IList DefaultEntryTagsSelector(ServiceEntry entry) + { + return new string[] { entry.Type.Name }; + } + + private IList DefaultTagsSelector(ApiDescription apiDescription) + { +#if (!NET6_0_OR_GREATER) + return new[] { apiDescription.ActionDescriptor.RouteValues["controller"] }; +#else + var actionDescriptor = apiDescription.ActionDescriptor; + var tagsMetadata = actionDescriptor.EndpointMetadata?.LastOrDefault(m => m is ITagsMetadata) as ITagsMetadata; + if (tagsMetadata != null) + { + return new List(tagsMetadata.Tags); + } + return new[] { apiDescription.ActionDescriptor.RouteValues["controller"] }; +#endif + } + + private string DefaultSortKeySelector(ApiDescription apiDescription) + { + return TagsSelector(apiDescription).First(); + } + } +} diff --git a/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/XmlComments/MethodInfoExtensions.cs b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/XmlComments/MethodInfoExtensions.cs new file mode 100644 index 000000000..5cabf7b5a --- /dev/null +++ b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/XmlComments/MethodInfoExtensions.cs @@ -0,0 +1,26 @@ +using System.Linq; +using System.Reflection; + +namespace Surging.Core.Swagger_V5.SwaggerGen +{ + public static class MethodInfoExtensions + { + public static MethodInfo GetUnderlyingGenericTypeMethod(this MethodInfo constructedTypeMethod) + { + var constructedType = constructedTypeMethod.DeclaringType; + var genericTypeDefinition = constructedType.GetGenericTypeDefinition(); + + // Retrieve list of candidate methods that match name and parameter count + var candidateMethods = genericTypeDefinition.GetMethods() + .Where(m => + { + return (m.Name == constructedTypeMethod.Name) + && (m.GetParameters().Length == constructedTypeMethod.GetParameters().Length); + }); + + + // If inconclusive, just return null + return (candidateMethods.Count() == 1) ? candidateMethods.First() : null; + } + } +} diff --git a/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/XmlComments/XmlCommentsDocumentFilter.cs b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/XmlComments/XmlCommentsDocumentFilter.cs new file mode 100644 index 000000000..4b9e6c519 --- /dev/null +++ b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/XmlComments/XmlCommentsDocumentFilter.cs @@ -0,0 +1,54 @@ +using System.Xml.XPath; +using System.Linq; +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.OpenApi.Models; +using System; + +namespace Surging.Core.Swagger_V5.SwaggerGen +{ + public class XmlCommentsDocumentFilter : IDocumentFilter + { + private const string MemberXPath = "/doc/members/member[@name='{0}']"; + private const string SummaryTag = "summary"; + + private readonly XPathNavigator _xmlNavigator; + + public XmlCommentsDocumentFilter(XPathDocument xmlDoc) + { + _xmlNavigator = xmlDoc.CreateNavigator(); + } + + public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) + { + // Collect (unique) controller names and types in a dictionary + var controllerNamesAndTypes = context.ApiDescriptions + .Select(apiDesc => apiDesc.ActionDescriptor as ControllerActionDescriptor) + .Where(actionDesc => actionDesc != null) + .GroupBy(actionDesc => actionDesc.ControllerName) + .Select(group => new KeyValuePair(group.Key, group.First().ControllerTypeInfo.AsType())); + + foreach (var nameAndType in controllerNamesAndTypes) + { + var memberName = XmlCommentsNodeNameHelper.GetMemberNameForType(nameAndType.Value); + var typeNode = _xmlNavigator.SelectSingleNode(string.Format(MemberXPath, memberName)); + + if (typeNode != null) + { + var summaryNode = typeNode.SelectSingleNode(SummaryTag); + if (summaryNode != null) + { + if (swaggerDoc.Tags == null) + swaggerDoc.Tags = new List(); + + swaggerDoc.Tags.Add(new OpenApiTag + { + Name = nameAndType.Key, + Description = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml) + }); + } + } + } + } + } +} diff --git a/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/XmlComments/XmlCommentsNodeNameHelper.cs b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/XmlComments/XmlCommentsNodeNameHelper.cs new file mode 100644 index 000000000..b8af41b16 --- /dev/null +++ b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/XmlComments/XmlCommentsNodeNameHelper.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace Surging.Core.Swagger_V5.SwaggerGen +{ + public class XmlCommentsNodeNameHelper + { + public static string GetMemberNameForMethod(MethodInfo method) + { + var builder = new StringBuilder("M:"); + + builder.Append(QualifiedNameFor(method.DeclaringType)); + builder.Append($".{method.Name}"); + + var parameters = method.GetParameters(); + if (parameters.Any()) + { + var parametersNames = parameters.Select(p => + { + return p.ParameterType.IsGenericParameter + ? $"`{p.ParameterType.GenericParameterPosition}" + : QualifiedNameFor(p.ParameterType, expandGenericArgs: true); + }); + builder.Append($"({string.Join(",", parametersNames)})"); + } + + return builder.ToString(); + } + + public static string GetMemberNameForType(Type type) + { + var builder = new StringBuilder("T:"); + builder.Append(QualifiedNameFor(type)); + + return builder.ToString(); + } + + public static string GetMemberNameForFieldOrProperty(MemberInfo fieldOrPropertyInfo) + { + var builder = new StringBuilder(((fieldOrPropertyInfo.MemberType & MemberTypes.Field) != 0) ? "F:" : "P:"); + builder.Append(QualifiedNameFor(fieldOrPropertyInfo.DeclaringType)); + builder.Append($".{fieldOrPropertyInfo.Name}"); + + return builder.ToString(); + } + + private static string QualifiedNameFor(Type type, bool expandGenericArgs = false) + { + if (type.IsArray) + return $"{QualifiedNameFor(type.GetElementType(), expandGenericArgs)}[]"; + + var builder = new StringBuilder(); + + if (!string.IsNullOrEmpty(type.Namespace)) + builder.Append($"{type.Namespace}."); + + if (type.IsNested) + { + builder.Append($"{string.Join(".", GetNestedTypeNames(type))}."); + } + + if (type.IsConstructedGenericType && expandGenericArgs) + { + var nameSansGenericArgs = type.Name.Split('`').First(); + builder.Append(nameSansGenericArgs); + + var genericArgsNames = type.GetGenericArguments().Select(t => + { + return t.IsGenericParameter + ? $"`{t.GenericParameterPosition}" + : QualifiedNameFor(t, true); + }); + + builder.Append($"{{{string.Join(",", genericArgsNames)}}}"); + } + else + { + builder.Append(type.Name); + } + + return builder.ToString(); + } + + private static IEnumerable GetNestedTypeNames(Type type) + { + if (!type.IsNested || type.DeclaringType == null) yield break; + + foreach (var nestedTypeName in GetNestedTypeNames(type.DeclaringType)) + { + yield return nestedTypeName; + } + + yield return type.DeclaringType.Name; + } + } +} diff --git a/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/XmlComments/XmlCommentsOperationFilter.cs b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/XmlComments/XmlCommentsOperationFilter.cs new file mode 100644 index 000000000..496f40f0f --- /dev/null +++ b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/XmlComments/XmlCommentsOperationFilter.cs @@ -0,0 +1,71 @@ +using System; +using System.Reflection; +using System.Xml.XPath; +using Microsoft.OpenApi.Models; + +namespace Surging.Core.Swagger_V5.SwaggerGen +{ + public class XmlCommentsOperationFilter : IOperationFilter + { + private readonly XPathNavigator _xmlNavigator; + + public XmlCommentsOperationFilter(XPathDocument xmlDoc) + { + _xmlNavigator = xmlDoc.CreateNavigator(); + } + + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + if (context.MethodInfo == null) return; + + // If method is from a constructed generic type, look for comments from the generic type method + var targetMethod = context.MethodInfo.DeclaringType.IsConstructedGenericType + ? context.MethodInfo.GetUnderlyingGenericTypeMethod() + : context.MethodInfo; + + if (targetMethod == null) return; + + ApplyControllerTags(operation, targetMethod.DeclaringType); + ApplyMethodTags(operation, targetMethod); + } + + private void ApplyControllerTags(OpenApiOperation operation, Type controllerType) + { + var typeMemberName = XmlCommentsNodeNameHelper.GetMemberNameForType(controllerType); + var responseNodes = _xmlNavigator.Select($"/doc/members/member[@name='{typeMemberName}']/response"); + ApplyResponseTags(operation, responseNodes); + } + + private void ApplyMethodTags(OpenApiOperation operation, MethodInfo methodInfo) + { + var methodMemberName = XmlCommentsNodeNameHelper.GetMemberNameForMethod(methodInfo); + var methodNode = _xmlNavigator.SelectSingleNode($"/doc/members/member[@name='{methodMemberName}']"); + + if (methodNode == null) return; + + var summaryNode = methodNode.SelectSingleNode("summary"); + if (summaryNode != null) + operation.Summary = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml); + + var remarksNode = methodNode.SelectSingleNode("remarks"); + if (remarksNode != null) + operation.Description = XmlCommentsTextHelper.Humanize(remarksNode.InnerXml); + + var responseNodes = methodNode.Select("response"); + ApplyResponseTags(operation, responseNodes); + } + + private void ApplyResponseTags(OpenApiOperation operation, XPathNodeIterator responseNodes) + { + while (responseNodes.MoveNext()) + { + var code = responseNodes.Current.GetAttribute("code", ""); + var response = operation.Responses.ContainsKey(code) + ? operation.Responses[code] + : operation.Responses[code] = new OpenApiResponse(); + + response.Description = XmlCommentsTextHelper.Humanize(responseNodes.Current.InnerXml); + } + } + } +} \ No newline at end of file diff --git a/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/XmlComments/XmlCommentsParameterFilter.cs b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/XmlComments/XmlCommentsParameterFilter.cs new file mode 100644 index 000000000..f0f4d6174 --- /dev/null +++ b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/XmlComments/XmlCommentsParameterFilter.cs @@ -0,0 +1,82 @@ +using System.Reflection; +using System.Xml.XPath; +using Microsoft.OpenApi.Models; + +namespace Surging.Core.Swagger_V5.SwaggerGen +{ + public class XmlCommentsParameterFilter : IParameterFilter + { + private XPathNavigator _xmlNavigator; + + public XmlCommentsParameterFilter(XPathDocument xmlDoc) + { + _xmlNavigator = xmlDoc.CreateNavigator(); + } + + public void Apply(OpenApiParameter parameter, ParameterFilterContext context) + { + if (context.PropertyInfo != null) + { + ApplyPropertyTags(parameter, context); + } + else if (context.ParameterInfo != null) + { + ApplyParamTags(parameter, context); + } + } + + private void ApplyPropertyTags(OpenApiParameter parameter, ParameterFilterContext context) + { + var propertyMemberName = XmlCommentsNodeNameHelper.GetMemberNameForFieldOrProperty(context.PropertyInfo); + var propertyNode = _xmlNavigator.SelectSingleNode($"/doc/members/member[@name='{propertyMemberName}']"); + + if (propertyNode == null) return; + + var summaryNode = propertyNode.SelectSingleNode("summary"); + if (summaryNode != null) + { + parameter.Description = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml); + parameter.Schema.Description = null; // no need to duplicate + } + + var exampleNode = propertyNode.SelectSingleNode("example"); + if (exampleNode == null) return; + + var exampleAsJson = (parameter.Schema?.ResolveType(context.SchemaRepository) == "string") + ? $"\"{exampleNode.ToString()}\"" + : exampleNode.ToString(); + + parameter.Example = OpenApiAnyFactory.CreateFromJson(exampleAsJson); + } + + private void ApplyParamTags(OpenApiParameter parameter, ParameterFilterContext context) + { + if (!(context.ParameterInfo.Member is MethodInfo methodInfo)) return; + + // If method is from a constructed generic type, look for comments from the generic type method + var targetMethod = methodInfo.DeclaringType.IsConstructedGenericType + ? methodInfo.GetUnderlyingGenericTypeMethod() + : methodInfo; + + if (targetMethod == null) return; + + var methodMemberName = XmlCommentsNodeNameHelper.GetMemberNameForMethod(targetMethod); + var paramNode = _xmlNavigator.SelectSingleNode( + $"/doc/members/member[@name='{methodMemberName}']/param[@name='{context.ParameterInfo.Name}']"); + + if (paramNode != null) + { + parameter.Description = XmlCommentsTextHelper.Humanize(paramNode.InnerXml); + + var example = paramNode.GetAttribute("example", ""); + if (string.IsNullOrEmpty(example)) return; + + var exampleAsJson = (parameter.Schema?.ResolveType(context.SchemaRepository) == "string") + ? $"\"{example}\"" + : example; + + parameter.Example = OpenApiAnyFactory.CreateFromJson(exampleAsJson); + } + } + } +} diff --git a/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/XmlComments/XmlCommentsRequestBodyFilter.cs b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/XmlComments/XmlCommentsRequestBodyFilter.cs new file mode 100644 index 000000000..dfbbbf319 --- /dev/null +++ b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/XmlComments/XmlCommentsRequestBodyFilter.cs @@ -0,0 +1,94 @@ +using System.Reflection; +using System.Xml.XPath; +using Microsoft.OpenApi.Models; + +namespace Surging.Core.Swagger_V5.SwaggerGen +{ + public class XmlCommentsRequestBodyFilter : IRequestBodyFilter + { + private readonly XPathNavigator _xmlNavigator; + + public XmlCommentsRequestBodyFilter(XPathDocument xmlDoc) + { + _xmlNavigator = xmlDoc.CreateNavigator(); + } + + public void Apply(OpenApiRequestBody requestBody, RequestBodyFilterContext context) + { + var bodyParameterDescription = context.BodyParameterDescription; + + if (bodyParameterDescription == null) return; + + var propertyInfo = bodyParameterDescription.PropertyInfo(); + if (propertyInfo != null) + { + ApplyPropertyTags(requestBody, context, propertyInfo); + return; + } + + var parameterInfo = bodyParameterDescription.ParameterInfo(); + if (parameterInfo != null) + { + ApplyParamTags(requestBody, context, parameterInfo); + return; + } + } + + private void ApplyPropertyTags(OpenApiRequestBody requestBody, RequestBodyFilterContext context, PropertyInfo propertyInfo) + { + var propertyMemberName = XmlCommentsNodeNameHelper.GetMemberNameForFieldOrProperty(propertyInfo); + var propertyNode = _xmlNavigator.SelectSingleNode($"/doc/members/member[@name='{propertyMemberName}']"); + + if (propertyNode == null) return; + + var summaryNode = propertyNode.SelectSingleNode("summary"); + if (summaryNode != null) + requestBody.Description = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml); + + var exampleNode = propertyNode.SelectSingleNode("example"); + if (exampleNode == null) return; + + foreach (var mediaType in requestBody.Content.Values) + { + var exampleAsJson = (mediaType.Schema?.ResolveType(context.SchemaRepository) == "string") + ? $"\"{exampleNode.ToString()}\"" + : exampleNode.ToString(); + + mediaType.Example = OpenApiAnyFactory.CreateFromJson(exampleAsJson); + } + } + + private void ApplyParamTags(OpenApiRequestBody requestBody, RequestBodyFilterContext context, ParameterInfo parameterInfo) + { + if (!(parameterInfo.Member is MethodInfo methodInfo)) return; + + // If method is from a constructed generic type, look for comments from the generic type method + var targetMethod = methodInfo.DeclaringType.IsConstructedGenericType + ? methodInfo.GetUnderlyingGenericTypeMethod() + : methodInfo; + + if (targetMethod == null) return; + + var methodMemberName = XmlCommentsNodeNameHelper.GetMemberNameForMethod(targetMethod); + var paramNode = _xmlNavigator.SelectSingleNode( + $"/doc/members/member[@name='{methodMemberName}']/param[@name='{parameterInfo.Name}']"); + + if (paramNode != null) + { + requestBody.Description = XmlCommentsTextHelper.Humanize(paramNode.InnerXml); + + var example = paramNode.GetAttribute("example", ""); + if (string.IsNullOrEmpty(example)) return; + + foreach (var mediaType in requestBody.Content.Values) + { + var exampleAsJson = (mediaType.Schema?.ResolveType(context.SchemaRepository) == "string") + ? $"\"{example}\"" + : example; + + mediaType.Example = OpenApiAnyFactory.CreateFromJson(exampleAsJson); + } + } + } + } +} diff --git a/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/XmlComments/XmlCommentsSchemaFilter.cs b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/XmlComments/XmlCommentsSchemaFilter.cs new file mode 100644 index 000000000..f69b9fac7 --- /dev/null +++ b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/XmlComments/XmlCommentsSchemaFilter.cs @@ -0,0 +1,59 @@ +using System; +using System.Xml.XPath; +using Microsoft.OpenApi.Models; + +namespace Surging.Core.Swagger_V5.SwaggerGen +{ + public class XmlCommentsSchemaFilter : ISchemaFilter + { + private readonly XPathNavigator _xmlNavigator; + + public XmlCommentsSchemaFilter(XPathDocument xmlDoc) + { + _xmlNavigator = xmlDoc.CreateNavigator(); + } + + public void Apply(OpenApiSchema schema, SchemaFilterContext context) + { + ApplyTypeTags(schema, context.Type); + + if (context.MemberInfo != null) + { + ApplyMemberTags(schema, context); + } + } + + private void ApplyTypeTags(OpenApiSchema schema, Type type) + { + var typeMemberName = XmlCommentsNodeNameHelper.GetMemberNameForType(type); + var typeSummaryNode = _xmlNavigator.SelectSingleNode($"/doc/members/member[@name='{typeMemberName}']/summary"); + + if (typeSummaryNode != null) + { + schema.Description = XmlCommentsTextHelper.Humanize(typeSummaryNode.InnerXml); + } + } + + private void ApplyMemberTags(OpenApiSchema schema, SchemaFilterContext context) + { + var fieldOrPropertyMemberName = XmlCommentsNodeNameHelper.GetMemberNameForFieldOrProperty(context.MemberInfo); + var fieldOrPropertyNode = _xmlNavigator.SelectSingleNode($"/doc/members/member[@name='{fieldOrPropertyMemberName}']"); + + if (fieldOrPropertyNode == null) return; + + var summaryNode = fieldOrPropertyNode.SelectSingleNode("summary"); + if (summaryNode != null) + schema.Description = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml); + + var exampleNode = fieldOrPropertyNode.SelectSingleNode("example"); + if (exampleNode != null) + { + var exampleAsJson = (schema.ResolveType(context.SchemaRepository) == "string") && !exampleNode.Value.Equals("null") + ? $"\"{exampleNode.ToString()}\"" + : exampleNode.ToString(); + + schema.Example = OpenApiAnyFactory.CreateFromJson(exampleAsJson); + } + } + } +} diff --git a/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/XmlComments/XmlCommentsTextHelper.cs b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/XmlComments/XmlCommentsTextHelper.cs new file mode 100644 index 000000000..856f74499 --- /dev/null +++ b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerGen/XmlComments/XmlCommentsTextHelper.cs @@ -0,0 +1,116 @@ +using System; +using System.Linq; +using System.Net; +using System.Text.RegularExpressions; + +namespace Surging.Core.Swagger_V5.SwaggerGen +{ + public static class XmlCommentsTextHelper + { + private static Regex RefTagPattern = new Regex(@"<(see|paramref) (name|cref)=""([TPF]{1}:)?(?.+?)"" ?/>"); + private static Regex CodeTagPattern = new Regex(@"(?.+?)"); + private static Regex MultilineCodeTagPattern = new Regex(@"(?.+?)", RegexOptions.Singleline); + private static Regex ParaTagPattern = new Regex(@"(?.+?)", RegexOptions.Singleline); + + public static string Humanize(string text) + { + if (text == null) + throw new ArgumentNullException("text"); + + //Call DecodeXml at last to avoid entities like < and > to break valid xml + + return text + .NormalizeIndentation() + .HumanizeRefTags() + .HumanizeCodeTags() + .HumanizeMultilineCodeTags() + .HumanizeParaTags() + .DecodeXml(); + } + + private static string NormalizeIndentation(this string text) + { + string[] lines = text.Split('\n'); + string padding = GetCommonLeadingWhitespace(lines); + + int padLen = padding == null ? 0 : padding.Length; + + // remove leading padding from each line + for (int i = 0, l = lines.Length; i < l; ++i) + { + string line = lines[i].TrimEnd('\r'); // remove trailing '\r' + + if (padLen != 0 && line.Length >= padLen && line.Substring(0, padLen) == padding) + line = line.Substring(padLen); + + lines[i] = line; + } + + // remove leading empty lines, but not all leading padding + // remove all trailing whitespace, regardless + return string.Join("\r\n", lines.SkipWhile(x => string.IsNullOrWhiteSpace(x))).TrimEnd(); + } + + private static string GetCommonLeadingWhitespace(string[] lines) + { + if (null == lines) + throw new ArgumentException("lines"); + + if (lines.Length == 0) + return null; + + string[] nonEmptyLines = lines + .Where(x => !string.IsNullOrWhiteSpace(x)) + .ToArray(); + + if (nonEmptyLines.Length < 1) + return null; + + int padLen = 0; + + // use the first line as a seed, and see what is shared over all nonEmptyLines + string seed = nonEmptyLines[0]; + for (int i = 0, l = seed.Length; i < l; ++i) + { + if (!char.IsWhiteSpace(seed, i)) + break; + + if (nonEmptyLines.Any(line => line[i] != seed[i])) + break; + + ++padLen; + } + + if (padLen > 0) + return seed.Substring(0, padLen); + + return null; + } + + private static string HumanizeRefTags(this string text) + { + return RefTagPattern.Replace(text, (match) => match.Groups["display"].Value); + } + + private static string HumanizeCodeTags(this string text) + { + return CodeTagPattern.Replace(text, (match) => "`" + match.Groups["display"].Value + "`"); + } + + private static string HumanizeMultilineCodeTags(this string text) + { + return MultilineCodeTagPattern.Replace(text, (match) => "```" + match.Groups["display"].Value + "```"); + } + + private static string HumanizeParaTags(this string text) + { + return ParaTagPattern.Replace(text, (match) => "
" + match.Groups["display"].Value); + } + + private static string DecodeXml(this string text) + { + return WebUtility.HtmlDecode(text); + } + + } +} \ No newline at end of file diff --git a/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerModule.cs b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerModule.cs new file mode 100644 index 000000000..a62b4e340 --- /dev/null +++ b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerModule.cs @@ -0,0 +1,113 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Surging.Core.CPlatform; +using Surging.Core.CPlatform.Module; +using Surging.Core.CPlatform.Runtime.Server; +using Surging.Core.KestrelHttpServer; +using Surging.Core.Swagger_V5.Builder; +using Surging.Core.Swagger_V5.Internal; +using Surging.Core.Swagger_V5.SwaggerUI; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using Surging.Core.Swagger_V5.DependencyInjection; +using Surging.Core.Swagger_V5.SwaggerGen.Filters; +using Microsoft.OpenApi.Models; +using Surging.Core.Swagger_V5.Swagger.Model; + +namespace Surging.Core.Swagger_V5 +{ + public class SwaggerModule: KestrelHttpModule + { + private IServiceSchemaProvider _serviceSchemaProvider; + private IServiceEntryProvider _serviceEntryProvider; + + public override void Initialize(AppModuleContext context) + { + var serviceProvider = context.ServiceProvoider; + _serviceSchemaProvider = serviceProvider.GetInstances(); + _serviceEntryProvider = serviceProvider.GetInstances(); + } + + public override void Initialize(ApplicationInitializationContext context) + { + var info = AppConfig.SwaggerConfig.Info == null + ? AppConfig.SwaggerOptions : AppConfig.SwaggerConfig.Info; + if (info != null) + { + context.Builder.UseSwagger(); + context.Builder.UseSwaggerUI(c => + { + c.ShowExtensions(); + var areaName = AppConfig.SwaggerConfig.Options?.IngressName; + c.SwaggerEndpoint($"../swagger/{info.Version}/swagger.json", info.Title, areaName); + c.SwaggerEndpoint(_serviceEntryProvider.GetALLEntries(), areaName); + }); + } + } + + public override void RegisterBuilder(ConfigurationContext context) + { + var serviceCollection = context.Services; + var info = AppConfig.SwaggerConfig.Info == null + ? AppConfig.SwaggerOptions : AppConfig.SwaggerConfig.Info; + var swaggerOptions = AppConfig.SwaggerConfig.Options; + if (info != null) + { + serviceCollection.AddSwaggerGen(options => + { + options.OperationFilter(); + + options.SwaggerDoc(info.Version, new OpenApiInfo + { + + Title = info.Title, + Contact = new OpenApiContact() { Email = info.Contact.Email, Name = info.Contact.Name, Url = new Uri(info.Contact.Url) }, + Description = info.Description, + License = new OpenApiLicense() { Name = info.License.Name, Url = new Uri(info.License.Url) }, + TermsOfService = info.TermsOfService==null?null: new Uri(info.TermsOfService), + Version = info.Version + }); + + if (swaggerOptions != null && swaggerOptions.IgnoreFullyQualified) + options.IgnoreFullyQualified(); + options.GenerateSwaggerDoc(_serviceEntryProvider.GetALLEntries()); + options.DocInclusionPredicateV2((docName, apiDesc) => + { + if (docName == info.Version) + return true; + var assembly = apiDesc.Type.Assembly; + + var title = assembly + .GetCustomAttributes(true) + .OfType(); + + return title.Any(v => v.Title == docName); + }); + var xmlPaths = _serviceSchemaProvider.GetSchemaFilesPath(); + foreach (var xmlPath in xmlPaths) + options.IncludeXmlComments(xmlPath); + }); + } + } + + /// + /// Inject dependent third-party components + /// + /// + protected override void RegisterBuilder(ContainerBuilderWrapper builder) + { + var section = CPlatform.AppConfig.GetSection("Swagger"); + if (section.Exists()) + { + AppConfig.SwaggerOptions = section.Get(); + AppConfig.SwaggerConfig = section.Get(); + } + builder.RegisterType(typeof(DefaultServiceSchemaProvider)).As(typeof(IServiceSchemaProvider)).SingleInstance(); + + } + } +} diff --git a/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerUI/SwaggerUIBuilderExtensions.cs b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerUI/SwaggerUIBuilderExtensions.cs new file mode 100644 index 000000000..8b62602f5 --- /dev/null +++ b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerUI/SwaggerUIBuilderExtensions.cs @@ -0,0 +1,48 @@ +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Surging.Core.Swagger_V5.SwaggerUI; + +#if NETSTANDARD2_0 +using IWebHostEnvironment = Microsoft.AspNetCore.Hosting.IHostingEnvironment; +#endif + +namespace Surging.Core.Swagger_V5.Builder +{ + public static class SwaggerUIBuilderExtensions + { + /// + /// Register the SwaggerUI middleware with provided options + /// + public static IApplicationBuilder UseSwaggerUI(this IApplicationBuilder app, SwaggerUIOptions options) + { + return app.UseMiddleware(options); + } + + /// + /// Register the SwaggerUI middleware with optional setup action for DI-injected options + /// + public static IApplicationBuilder UseSwaggerUI( + this IApplicationBuilder app, + Action setupAction = null) + { + SwaggerUIOptions options; + using (var scope = app.ApplicationServices.CreateScope()) + { + options = scope.ServiceProvider.GetRequiredService>().Value; + setupAction?.Invoke(options); + } + + // To simplify the common case, use a default that will work with the SwaggerMiddleware defaults + if (options.ConfigObject.Urls == null) + { + var hostingEnv = app.ApplicationServices.GetRequiredService(); + options.ConfigObject.Urls = new[] { new UrlDescriptor { Name = $"{hostingEnv.ApplicationName} v1", Url = "v1/swagger.json" } }; + } + + return app.UseSwaggerUI(options); + } + } +} diff --git a/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerUI/SwaggerUIMiddleware.cs b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerUI/SwaggerUIMiddleware.cs new file mode 100644 index 000000000..7aad70466 --- /dev/null +++ b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerUI/SwaggerUIMiddleware.cs @@ -0,0 +1,133 @@ +using System; +using System.Reflection; +using System.Threading.Tasks; +using System.Text.RegularExpressions; +using System.Text; +using System.Collections.Generic; +using System.IO; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.FileProviders; +using Microsoft.AspNetCore.StaticFiles; +using Microsoft.AspNetCore.Http.Extensions; +using System.Linq; + +#if NETSTANDARD2_0 +using IWebHostEnvironment = Microsoft.AspNetCore.Hosting.IHostingEnvironment; +#endif + +namespace Surging.Core.Swagger_V5.SwaggerUI +{ + public class SwaggerUIMiddleware + { + private const string EmbeddedFileNamespace = "Surging.Core.Swagger_V5.SwaggerUI.node_modules.swagger_ui_dist"; + + private readonly SwaggerUIOptions _options; + private readonly StaticFileMiddleware _staticFileMiddleware; + private readonly JsonSerializerOptions _jsonSerializerOptions; + + public SwaggerUIMiddleware( + RequestDelegate next, + IWebHostEnvironment hostingEnv, + ILoggerFactory loggerFactory, + SwaggerUIOptions options) + { + _options = options ?? new SwaggerUIOptions(); + + _staticFileMiddleware = CreateStaticFileMiddleware(next, hostingEnv, loggerFactory, options); + + _jsonSerializerOptions = new JsonSerializerOptions(); +#if NET6_0 + _jsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; +#else + _jsonSerializerOptions.IgnoreNullValues = true; +#endif + _jsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + _jsonSerializerOptions.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, false)); + } + + public async Task Invoke(HttpContext httpContext) + { + var httpMethod = httpContext.Request.Method; + var path = httpContext.Request.Path.Value; + + // If the RoutePrefix is requested (with or without trailing slash), redirect to index URL + if (httpMethod == "GET" && _options.RoutePrefix.Equals(path.Trim('/'), StringComparison.OrdinalIgnoreCase)) + { + // Use relative redirect to support proxy environments + var relativeIndexUrl = string.IsNullOrEmpty(path) || path.EndsWith("/") + ? "index.html" + : $"{path.Split('/').Last()}/index.html"; + + RespondWithRedirect(httpContext.Response, relativeIndexUrl); + return; + } + + if (httpMethod == "GET" && $"/{_options.RoutePrefix}/index.html".Equals(path, StringComparison.OrdinalIgnoreCase)) + { + await RespondWithIndexHtml(httpContext.Response); + return; + } + + await _staticFileMiddleware.Invoke(httpContext); + } + + private StaticFileMiddleware CreateStaticFileMiddleware( + RequestDelegate next, + IWebHostEnvironment hostingEnv, + ILoggerFactory loggerFactory, + SwaggerUIOptions options) + { + var staticFileOptions = new StaticFileOptions + { + RequestPath = string.IsNullOrEmpty(options.RoutePrefix) ? string.Empty : $"/{options.RoutePrefix}", + FileProvider = new EmbeddedFileProvider(typeof(SwaggerUIMiddleware).GetTypeInfo().Assembly, EmbeddedFileNamespace), + }; + + return new StaticFileMiddleware(next, hostingEnv, Options.Create(staticFileOptions), loggerFactory); + } + + private void RespondWithRedirect(HttpResponse response, string location) + { + response.StatusCode = 301; + response.Headers["Location"] = location; + } + + private async Task RespondWithIndexHtml(HttpResponse response) + { + response.StatusCode = 200; + response.ContentType = "text/html;charset=utf-8"; + + using (var stream = _options.IndexStream()) + { + using var reader = new StreamReader(stream); + + // Inject arguments before writing to response + var htmlBuilder = new StringBuilder(await reader.ReadToEndAsync()); + foreach (var entry in GetIndexArguments()) + { + htmlBuilder.Replace(entry.Key, entry.Value); + } + + await response.WriteAsync(htmlBuilder.ToString(), Encoding.UTF8); + } + } + + private IDictionary GetIndexArguments() + { + return new Dictionary() + { + { "%(DocumentTitle)", _options.DocumentTitle }, + { "%(HeadContent)", _options.HeadContent }, + { "%(ConfigObject)", JsonSerializer.Serialize(_options.ConfigObject, _jsonSerializerOptions) }, + { "%(OAuthConfigObject)", JsonSerializer.Serialize(_options.OAuthConfigObject, _jsonSerializerOptions) }, + { "%(Interceptors)", JsonSerializer.Serialize(_options.Interceptors) }, + }; + } + } +} diff --git a/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerUI/SwaggerUIOptions.cs b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerUI/SwaggerUIOptions.cs new file mode 100644 index 000000000..a8b54ba71 --- /dev/null +++ b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerUI/SwaggerUIOptions.cs @@ -0,0 +1,249 @@ +using System; +using System.IO; +using System.Reflection; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Serialization; + +namespace Surging.Core.Swagger_V5.SwaggerUI +{ + public class SwaggerUIOptions + { + /// + /// Gets or sets a route prefix for accessing the swagger-ui + /// + public string RoutePrefix { get; set; } = "swagger"; + + /// + /// Gets or sets a Stream function for retrieving the swagger-ui page + /// + public Func IndexStream { get; set; } = () => typeof(SwaggerUIOptions).GetTypeInfo().Assembly + .GetManifestResourceStream("Surging.Core.Swagger_V5.SwaggerUI.index.html"); + + /// + /// Gets or sets a title for the swagger-ui page + /// + public string DocumentTitle { get; set; } = "Swagger UI"; + + /// + /// Gets or sets additional content to place in the head of the swagger-ui page + /// + public string HeadContent { get; set; } = ""; + + /// + /// Gets the JavaScript config object, represented as JSON, that will be passed to the SwaggerUI + /// + public ConfigObject ConfigObject { get; set; } = new ConfigObject(); + + /// + /// Gets the JavaScript config object, represented as JSON, that will be passed to the initOAuth method + /// + public OAuthConfigObject OAuthConfigObject { get; set; } = new OAuthConfigObject(); + + /// + /// Gets the interceptor functions that define client-side request/response interceptors + /// + public InterceptorFunctions Interceptors { get; set; } = new InterceptorFunctions(); + } + + public class ConfigObject + { + /// + /// One or more Swagger JSON endpoints (url and name) to power the UI + /// + public IEnumerable Urls { get; set; } = null; + + /// + /// If set to true, enables deep linking for tags and operations + /// + public bool DeepLinking { get; set; } = false; + /// + /// If set to true, it persists authorization data and it would not be lost on browser close/refresh + /// + public bool PersistAuthorization { get; set; } = false; + + /// + /// Controls the display of operationId in operations list + /// + public bool DisplayOperationId { get; set; } = false; + + /// + /// The default expansion depth for models (set to -1 completely hide the models) + /// + public int DefaultModelsExpandDepth { get; set; } = 1; + + /// + /// The default expansion depth for the model on the model-example section + /// + public int DefaultModelExpandDepth { get; set; } = 1; + + /// + /// Controls how the model is shown when the API is first rendered. + /// (The user can always switch the rendering for a given model by clicking the 'Model' and 'Example Value' links) + /// + public ModelRendering DefaultModelRendering { get; set; } = ModelRendering.Example; + + /// + /// Controls the display of the request duration (in milliseconds) for Try-It-Out requests + /// + public bool DisplayRequestDuration { get; set; } = false; + + /// + /// Controls the default expansion setting for the operations and tags. + /// It can be 'list' (expands only the tags), 'full' (expands the tags and operations) or 'none' (expands nothing) + /// + public DocExpansion DocExpansion { get; set; } = DocExpansion.List; + + /// + /// If set, enables filtering. The top bar will show an edit box that you can use to filter the tagged operations + /// that are shown. Can be an empty string or specific value, in which case filtering will be enabled using that + /// value as the filter expression. Filtering is case sensitive matching the filter expression anywhere inside the tag + /// + public string Filter { get; set; } = null; + + /// + /// If set, limits the number of tagged operations displayed to at most this many. The default is to show all operations + /// + public int? MaxDisplayedTags { get; set; } = null; + + /// + /// Controls the display of vendor extension (x-) fields and values for Operations, Parameters, and Schema + /// + public bool ShowExtensions { get; set; } = false; + + /// + /// Controls the display of extensions (pattern, maxLength, minLength, maximum, minimum) fields and values for Parameters + /// + public bool ShowCommonExtensions { get; set; } = false; + + /// + /// OAuth redirect URL + /// + [JsonPropertyName("oauth2RedirectUrl")] + public string OAuth2RedirectUrl { get; set; } = null; + + /// + /// List of HTTP methods that have the Try it out feature enabled. + /// An empty array disables Try it out for all operations. This does not filter the operations from the display + /// + public IEnumerable SupportedSubmitMethods { get; set; } = Enum.GetValues(typeof(SubmitMethod)).Cast(); + + /// + /// Controls whether the "Try it out" section should be enabled by default. + /// + [JsonPropertyName("tryItOutEnabled")] + public bool TryItOutEnabled { get; set; } + + /// + /// By default, Swagger-UI attempts to validate specs against swagger.io's online validator. + /// You can use this parameter to set a different validator URL, for example for locally deployed validators (Validator Badge). + /// Setting it to null will disable validation + /// + public string ValidatorUrl { get; set; } = null; + + [JsonExtensionData] + public Dictionary AdditionalItems { get; set; } = new Dictionary(); + } + + public class UrlDescriptor + { + public string Url { get; set; } + + public string Name { get; set; } + } + + public enum ModelRendering + { + Example, + Model + } + + public enum DocExpansion + { + List, + Full, + None + } + + public enum SubmitMethod + { + Get, + Put, + Post, + Delete, + Options, + Head, + Patch, + Trace + } + + public class OAuthConfigObject + { + /// + /// Default clientId + /// + public string ClientId { get; set; } = null; + + /// + /// Default clientSecret + /// + public string ClientSecret { get; set; } = null; + + /// + /// Realm query parameter (for oauth1) added to authorizationUrl and tokenUrl + /// + public string Realm { get; set; } = null; + + /// + /// Application name, displayed in authorization popup + /// + public string AppName { get; set; } = null; + + /// + /// Scope separator for passing scopes, encoded before calling, default value is a space (encoded value %20) + /// + public string ScopeSeparator { get; set; } = " "; + + /// + /// String array of initially selected oauth scopes, default is empty array + /// + public IEnumerable Scopes { get; set; } = new string[] { }; + + /// + /// Additional query parameters added to authorizationUrl and tokenUrl + /// + public Dictionary AdditionalQueryStringParams { get; set; } = null; + + /// + /// Only activated for the accessCode flow. During the authorization_code request to the tokenUrl, + /// pass the Client Password using the HTTP Basic Authentication scheme + /// (Authorization header with Basic base64encode(client_id + client_secret)) + /// + public bool UseBasicAuthenticationWithAccessCodeGrant { get; set; } = false; + + /// + /// Only applies to authorizatonCode flows. Proof Key for Code Exchange brings enhanced security for OAuth public clients. + /// The default is false + /// + public bool UsePkceWithAuthorizationCodeGrant { get; set; } = false; + } + + public class InterceptorFunctions + { + /// + /// MUST be a valid Javascript function. + /// Function to intercept remote definition, "Try it out", and OAuth 2.0 requests. + /// Accepts one argument requestInterceptor(request) and must return the modified request, or a Promise that resolves to the modified request. + /// Ex: "function (req) { req.headers['MyCustomHeader'] = 'CustomValue'; return req; }" + /// + public string RequestInterceptorFunction { get; set; } + + /// + /// MUST be a valid Javascript function. + /// Function to intercept remote definition, "Try it out", and OAuth 2.0 responses. + /// Accepts one argument responseInterceptor(response) and must return the modified response, or a Promise that resolves to the modified response. + /// Ex: "function (res) { console.log(res); return res; }" + /// + public string ResponseInterceptorFunction { get; set; } + } +} \ No newline at end of file diff --git a/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerUI/SwaggerUIOptionsExtensions.cs b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerUI/SwaggerUIOptionsExtensions.cs new file mode 100644 index 000000000..6a7149912 --- /dev/null +++ b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerUI/SwaggerUIOptionsExtensions.cs @@ -0,0 +1,368 @@ +using System; +using System.Text; +using System.Linq; +using System.Collections.Generic; +using Surging.Core.Swagger_V5.SwaggerUI; +using Surging.Core.CPlatform.Runtime.Server; +using System.Reflection; +using Surging.Core.Swagger_V5.Swagger.Model; + +namespace Surging.Core.Swagger_V5.Builder +{ + public static class SwaggerUIOptionsExtensions + { + /// + /// Injects additional CSS stylesheets into the index.html page + /// + /// + /// A path to the stylesheet - i.e. the link "href" attribute + /// The target media - i.e. the link "media" attribute + public static void InjectStylesheet(this SwaggerUIOptions options, string path, string media = "screen") + { + var builder = new StringBuilder(options.HeadContent); + builder.AppendLine($""); + options.HeadContent = builder.ToString(); + } + + /// + /// Injects additional Javascript files into the index.html page + /// + /// + /// A path to the javascript - i.e. the script "src" attribute + /// The script type - i.e. the script "type" attribute + public static void InjectJavascript(this SwaggerUIOptions options, string path, string type = "text/javascript") + { + var builder = new StringBuilder(options.HeadContent); + builder.AppendLine($""); + options.HeadContent = builder.ToString(); + } + + /// + /// Adds Swagger JSON endpoints. Can be fully-qualified or relative to the UI page + /// + /// + /// Can be fully qualified or relative to the current host + /// The description that appears in the document selector drop-down + public static void SwaggerEndpoint(this SwaggerUIOptions options, string url, string name) + { + var urls = new List(options.ConfigObject.Urls ?? Enumerable.Empty()); + urls.Add(new UrlDescriptor { Url = url, Name = name} ); + options.ConfigObject.Urls = urls; + } + + public static void SwaggerEndpoint(this SwaggerUIOptions options, string url, string name, string areaName) + { + var urls = new List(options.ConfigObject.Urls ?? Enumerable.Empty()); + urls.Add(new UrlDescriptor { Url = string.IsNullOrEmpty(areaName) ? url : $"{areaName}{url}", Name = name }); + options.ConfigObject.Urls = urls; + } + + public static void SwaggerEndpoint(this SwaggerUIOptions options, IEnumerable entries, string areaName) + { + + var list = new List(); + var assemblies = entries.Select(p => p.Type.Assembly).Distinct(); + foreach (var assembly in assemblies) + { + var version = assembly + .GetCustomAttributes(true) + .OfType().FirstOrDefault(); + + var title = assembly + .GetCustomAttributes(true) + .OfType().FirstOrDefault(); + + var des = assembly + .GetCustomAttributes(true) + .OfType().FirstOrDefault(); + + if (version == null || title == null) + continue; + + var info = new Info() + { + Title = title.Title, + Version = version.Version, + Description = des?.Description, + + }; + options.SwaggerEndpoint($"../swagger/{info.Title}/swagger.json", info.Title, areaName); + } + } + + /// + /// Enables deep linking for tags and operations + /// + /// + public static void EnableDeepLinking(this SwaggerUIOptions options) + { + options.ConfigObject.DeepLinking = true; + } + /// + /// Enables persist authorization data + /// + /// + public static void EnablePersistAuthorization(this SwaggerUIOptions options) + { + options.ConfigObject.PersistAuthorization = true; + } + + /// + /// Controls the display of operationId in operations list + /// + /// + public static void DisplayOperationId(this SwaggerUIOptions options) + { + options.ConfigObject.DisplayOperationId = true; + } + + /// + /// The default expansion depth for models (set to -1 completely hide the models) + /// + /// + /// + public static void DefaultModelsExpandDepth(this SwaggerUIOptions options, int depth) + { + options.ConfigObject.DefaultModelsExpandDepth = depth; + } + + /// + /// The default expansion depth for the model on the model-example section + /// + /// + /// + public static void DefaultModelExpandDepth(this SwaggerUIOptions options, int depth) + { + options.ConfigObject.DefaultModelExpandDepth = depth; + } + + /// + /// Controls how the model is shown when the API is first rendered. + /// (The user can always switch the rendering for a given model by clicking the 'Model' and 'Example Value' links.) + /// + /// + /// + public static void DefaultModelRendering(this SwaggerUIOptions options, ModelRendering modelRendering) + { + options.ConfigObject.DefaultModelRendering = modelRendering; + } + + /// + /// Controls the display of the request duration (in milliseconds) for Try-It-Out requests + /// + /// + public static void DisplayRequestDuration(this SwaggerUIOptions options) + { + options.ConfigObject.DisplayRequestDuration = true; + } + + /// + /// Controls the default expansion setting for the operations and tags. + /// It can be 'List' (expands only the tags), 'Full' (expands the tags and operations) or 'None' (expands nothing) + /// + /// + /// + public static void DocExpansion(this SwaggerUIOptions options, DocExpansion docExpansion) + { + options.ConfigObject.DocExpansion = docExpansion; + } + + /// + /// Enables filtering. The top bar will show an edit box that you can use to filter the tagged operations that are shown. + /// If an expression is provided it will be used and applied initially. + /// Filtering is case sensitive matching the filter expression anywhere inside the tag + /// + /// + /// + public static void EnableFilter(this SwaggerUIOptions options, string expression = null) + { + options.ConfigObject.Filter = expression ?? ""; + } + + /// + /// Enables the "Try it out" section by default. + /// + /// + public static void EnableTryItOutByDefault(this SwaggerUIOptions options) + { + options.ConfigObject.TryItOutEnabled = true; + } + + /// + /// Limits the number of tagged operations displayed to at most this many. The default is to show all operations + /// + /// + /// + public static void MaxDisplayedTags(this SwaggerUIOptions options, int count) + { + options.ConfigObject.MaxDisplayedTags = count; + } + + /// + /// Controls the display of vendor extension (x-) fields and values for Operations, Parameters, and Schema + /// + /// + public static void ShowExtensions(this SwaggerUIOptions options) + { + options.ConfigObject.ShowExtensions = true; + } + + /// + /// Controls the display of extensions (pattern, maxLength, minLength, maximum, minimum) fields and values for Parameters + /// + /// + public static void ShowCommonExtensions(this SwaggerUIOptions options) + { + options.ConfigObject.ShowCommonExtensions = true; + } + + /// + /// List of HTTP methods that have the Try it out feature enabled. An empty array disables Try it out for all operations. + /// This does not filter the operations from the display + /// + /// + /// + public static void SupportedSubmitMethods(this SwaggerUIOptions options, params SubmitMethod[] submitMethods) + { + options.ConfigObject.SupportedSubmitMethods = submitMethods; + } + + /// + /// OAuth redirect URL + /// + /// + /// + public static void OAuth2RedirectUrl(this SwaggerUIOptions options, string url) + { + options.ConfigObject.OAuth2RedirectUrl = url; + } + + [Obsolete("The validator is disabled by default. Use EnableValidator to enable it")] + public static void ValidatorUrl(this SwaggerUIOptions options, string url) + { + options.ConfigObject.ValidatorUrl = url; + } + + /// + /// You can use this parameter to enable the swagger-ui's built-in validator (badge) functionality + /// Setting it to null will disable validation + /// + /// + /// + public static void EnableValidator(this SwaggerUIOptions options, string url = "https://online.swagger.io/validator") + { + options.ConfigObject.ValidatorUrl = url; + } + + /// + /// Default clientId + /// + /// + /// + public static void OAuthClientId(this SwaggerUIOptions options, string value) + { + options.OAuthConfigObject.ClientId = value; + } + + /// + /// Default clientSecret + /// + /// + /// + public static void OAuthClientSecret(this SwaggerUIOptions options, string value) + { + options.OAuthConfigObject.ClientSecret = value; + } + + /// + /// realm query parameter (for oauth1) added to authorizationUrl and tokenUrl + /// + /// + /// + public static void OAuthRealm(this SwaggerUIOptions options, string value) + { + options.OAuthConfigObject.Realm = value; + } + + /// + /// Application name, displayed in authorization popup + /// + /// + /// + public static void OAuthAppName(this SwaggerUIOptions options, string value) + { + options.OAuthConfigObject.AppName = value; + } + + /// + /// Scope separator for passing scopes, encoded before calling, default value is a space (encoded value %20) + /// + /// + /// + public static void OAuthScopeSeparator(this SwaggerUIOptions options, string value) + { + options.OAuthConfigObject.ScopeSeparator = value; + } + + /// + /// String array of initially selected oauth scopes, default is empty array + /// + public static void OAuthScopes(this SwaggerUIOptions options, params string[] scopes) + { + options.OAuthConfigObject.Scopes = scopes; + } + + /// + /// Additional query parameters added to authorizationUrl and tokenUrl + /// + /// + /// + public static void OAuthAdditionalQueryStringParams( + this SwaggerUIOptions options, + Dictionary value) + { + options.OAuthConfigObject.AdditionalQueryStringParams = value; + } + + /// + /// Only activated for the accessCode flow. During the authorization_code request to the tokenUrl, + /// pass the Client Password using the HTTP Basic Authentication scheme (Authorization header with + /// Basic base64encoded[client_id:client_secret]). The default is false + /// + /// + public static void OAuthUseBasicAuthenticationWithAccessCodeGrant(this SwaggerUIOptions options) + { + options.OAuthConfigObject.UseBasicAuthenticationWithAccessCodeGrant = true; + } + + /// + /// Only applies to authorizatonCode flows. Proof Key for Code Exchange brings enhanced security for OAuth public clients. + /// The default is false + /// + /// + public static void OAuthUsePkce(this SwaggerUIOptions options) + { + options.OAuthConfigObject.UsePkceWithAuthorizationCodeGrant = true; + } + + /// + /// Function to intercept remote definition, "Try it out", and OAuth 2.0 requests. + /// + /// + /// MUST be a valid Javascript function: (request: SwaggerRequest) => SwaggerRequest + public static void UseRequestInterceptor(this SwaggerUIOptions options, string value) + { + options.Interceptors.RequestInterceptorFunction = value; + } + + /// + /// Function to intercept remote definition, "Try it out", and OAuth 2.0 responses. + /// + /// + /// MUST be a valid Javascript function: (response: SwaggerResponse ) => SwaggerResponse + public static void UseResponseInterceptor(this SwaggerUIOptions options, string value) + { + options.Interceptors.ResponseInterceptorFunction = value; + } + } +} diff --git a/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerUI/index.html b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerUI/index.html new file mode 100644 index 000000000..c5360b285 --- /dev/null +++ b/src/Surging.Core/Surging.Core.Swagger_V5/SwaggerUI/index.html @@ -0,0 +1,117 @@ + + + + + + %(DocumentTitle) + + + + + %(HeadContent) + + + +
+ + + + + + + + + diff --git a/src/Surging.Core/Surging.Core.System/Surging.Core.System.csproj b/src/Surging.Core/Surging.Core.System/Surging.Core.System.csproj index 178e34c8b..c8a8ae4f5 100644 --- a/src/Surging.Core/Surging.Core.System/Surging.Core.System.csproj +++ b/src/Surging.Core/Surging.Core.System/Surging.Core.System.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 false diff --git a/src/Surging.Core/Surging.Core.Thrift/Surging.Core.Thrift.csproj b/src/Surging.Core/Surging.Core.Thrift/Surging.Core.Thrift.csproj index 73d935db3..f8018349f 100644 --- a/src/Surging.Core/Surging.Core.Thrift/Surging.Core.Thrift.csproj +++ b/src/Surging.Core/Surging.Core.Thrift/Surging.Core.Thrift.csproj @@ -1,7 +1,7 @@ - netcoreapp3.1 + net6.0 diff --git a/src/Surging.Core/Surging.Core.Zookeeper/Surging.Core.Zookeeper.csproj b/src/Surging.Core/Surging.Core.Zookeeper/Surging.Core.Zookeeper.csproj index 43a0e685a..2e33b55e8 100644 --- a/src/Surging.Core/Surging.Core.Zookeeper/Surging.Core.Zookeeper.csproj +++ b/src/Surging.Core/Surging.Core.Zookeeper/Surging.Core.Zookeeper.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 1.1.0.0 fanly surging Micro Service Framework diff --git a/src/Surging.IModuleServices/Surging.IModuleServices.Common/Surging.IModuleServices.Common.csproj b/src/Surging.IModuleServices/Surging.IModuleServices.Common/Surging.IModuleServices.Common.csproj index dcaedd59e..0cb81ae20 100644 --- a/src/Surging.IModuleServices/Surging.IModuleServices.Common/Surging.IModuleServices.Common.csproj +++ b/src/Surging.IModuleServices/Surging.IModuleServices.Common/Surging.IModuleServices.Common.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 true 测试模块 测试 diff --git a/src/Surging.IModuleServices/Surging.IModuleServices.Manger/Surging.IModuleServices.Manager.csproj b/src/Surging.IModuleServices/Surging.IModuleServices.Manger/Surging.IModuleServices.Manager.csproj index 3d62f6869..8b7f5860f 100644 --- a/src/Surging.IModuleServices/Surging.IModuleServices.Manger/Surging.IModuleServices.Manager.csproj +++ b/src/Surging.IModuleServices/Surging.IModuleServices.Manger/Surging.IModuleServices.Manager.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 diff --git a/src/Surging.Modules/Surging.Modules.Common/Surging.Modules.Common.csproj b/src/Surging.Modules/Surging.Modules.Common/Surging.Modules.Common.csproj index 0ac45f946..9f0590ca3 100644 --- a/src/Surging.Modules/Surging.Modules.Common/Surging.Modules.Common.csproj +++ b/src/Surging.Modules/Surging.Modules.Common/Surging.Modules.Common.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 false diff --git a/src/Surging.Modules/Surging.Modules.Manager/Surging.Modules.Manager.csproj b/src/Surging.Modules/Surging.Modules.Manager/Surging.Modules.Manager.csproj index 9d3ab30cc..742b6720a 100644 --- a/src/Surging.Modules/Surging.Modules.Manager/Surging.Modules.Manager.csproj +++ b/src/Surging.Modules/Surging.Modules.Manager/Surging.Modules.Manager.csproj @@ -1,7 +1,7 @@ - + - netcoreapp3.1 + net6.0 diff --git a/src/Surging.Services/Surging.Services.Client/Surging.Services.Client.csproj b/src/Surging.Services/Surging.Services.Client/Surging.Services.Client.csproj index e50e18a5c..7aa701c65 100644 --- a/src/Surging.Services/Surging.Services.Client/Surging.Services.Client.csproj +++ b/src/Surging.Services/Surging.Services.Client/Surging.Services.Client.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp3.1 + net6.0 true diff --git a/src/Surging.Services/Surging.Services.Server/Surging.Services.Server.csproj b/src/Surging.Services/Surging.Services.Server/Surging.Services.Server.csproj index 3b5ed37ec..63d4fa735 100644 --- a/src/Surging.Services/Surging.Services.Server/Surging.Services.Server.csproj +++ b/src/Surging.Services/Surging.Services.Server/Surging.Services.Server.csproj @@ -3,7 +3,7 @@ Exe false - netcoreapp3.1 + net6.0 true @@ -62,7 +62,7 @@ - + diff --git a/src/Surging.Tools/Surging.Tools.Cli/Surging.Tools.Cli.csproj b/src/Surging.Tools/Surging.Tools.Cli/Surging.Tools.Cli.csproj index b6e743300..57fe40873 100644 --- a/src/Surging.Tools/Surging.Tools.Cli/Surging.Tools.Cli.csproj +++ b/src/Surging.Tools/Surging.Tools.Cli/Surging.Tools.Cli.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp3.1 + net6.0 engine-cli diff --git a/src/Surging.Web/Surging.Web.csproj b/src/Surging.Web/Surging.Web.csproj index 8130113d3..1421b7fa7 100644 --- a/src/Surging.Web/Surging.Web.csproj +++ b/src/Surging.Web/Surging.Web.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 diff --git a/src/Surging.sln b/src/Surging.sln index 2c86a8c9c..822e4beed 100644 --- a/src/Surging.sln +++ b/src/Surging.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.28527.54 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31919.166 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Surging.Core.Common", "Surging.Core\Surging.Core.Common\Surging.Core.Common.csproj", "{F9B2DBAE-34D5-4CF0-9CBB-67A87580BB6A}" EndProject @@ -77,8 +77,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetty.Codecs.DNS", "DotN EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Surging.Core.DNS", "Surging.Core\Surging.Core.DNS\Surging.Core.DNS.csproj", "{3FA2B587-8490-4874-8318-84C1CBA2B111}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Surging.Core.Swagger", "Surging.Core\Surging.Core.Swagger\Surging.Core.Swagger.csproj", "{F824A598-F19F-4A32-A28E-FF2FE96EFC10}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Surging.Core.Abp", "Surging.Core\Surging.Core.Abp\Surging.Core.Abp.csproj", "{00BFD602-245D-4052-8B8C-7AA319BBD567}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Surging.Core.AutoMapper", "Surging.Core\Surging.Core.AutoMapper\Surging.Core.AutoMapper.csproj", "{8D796D5A-9FC6-491B-906D-50B2AABAECF3}" @@ -111,7 +109,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Surging.Core.Thrift", "Surg EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Surging.Tools", "Surging.Tools", "{78305463-1A38-4431-AFB0-0A0BE59D5A03}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Surging.Tools.Cli", "Surging.Tools\Surging.Tools.Cli\Surging.Tools.Cli.csproj", "{39B65A52-996E-4429-95DB-4731CD1EEAA9}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Surging.Tools.Cli", "Surging.Tools\Surging.Tools.Cli\Surging.Tools.Cli.csproj", "{39B65A52-996E-4429-95DB-4731CD1EEAA9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Surging.Core.Swagger_V5", "Surging.Core\Surging.Core.Swagger_V5\Surging.Core.Swagger_V5.csproj", "{4EF33237-E340-4041-9A83-11B012D3A736}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -243,10 +243,6 @@ Global {3FA2B587-8490-4874-8318-84C1CBA2B111}.Debug|Any CPU.Build.0 = Debug|Any CPU {3FA2B587-8490-4874-8318-84C1CBA2B111}.Release|Any CPU.ActiveCfg = Release|Any CPU {3FA2B587-8490-4874-8318-84C1CBA2B111}.Release|Any CPU.Build.0 = Release|Any CPU - {F824A598-F19F-4A32-A28E-FF2FE96EFC10}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F824A598-F19F-4A32-A28E-FF2FE96EFC10}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F824A598-F19F-4A32-A28E-FF2FE96EFC10}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F824A598-F19F-4A32-A28E-FF2FE96EFC10}.Release|Any CPU.Build.0 = Release|Any CPU {00BFD602-245D-4052-8B8C-7AA319BBD567}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {00BFD602-245D-4052-8B8C-7AA319BBD567}.Debug|Any CPU.Build.0 = Debug|Any CPU {00BFD602-245D-4052-8B8C-7AA319BBD567}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -307,6 +303,10 @@ Global {39B65A52-996E-4429-95DB-4731CD1EEAA9}.Debug|Any CPU.Build.0 = Debug|Any CPU {39B65A52-996E-4429-95DB-4731CD1EEAA9}.Release|Any CPU.ActiveCfg = Release|Any CPU {39B65A52-996E-4429-95DB-4731CD1EEAA9}.Release|Any CPU.Build.0 = Release|Any CPU + {4EF33237-E340-4041-9A83-11B012D3A736}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4EF33237-E340-4041-9A83-11B012D3A736}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4EF33237-E340-4041-9A83-11B012D3A736}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4EF33237-E340-4041-9A83-11B012D3A736}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -341,7 +341,6 @@ Global {A0502179-D200-49AE-9897-A6B189A75B45} = {E3ADE8DE-5F3A-4E76-B22F-2C9435AACA06} {A3B75BE5-A26A-46AC-9C1F-64CDCB2D413B} = {C76A8400-CEFF-4E68-B632-CC4E888C6A31} {3FA2B587-8490-4874-8318-84C1CBA2B111} = {E3ADE8DE-5F3A-4E76-B22F-2C9435AACA06} - {F824A598-F19F-4A32-A28E-FF2FE96EFC10} = {E3ADE8DE-5F3A-4E76-B22F-2C9435AACA06} {00BFD602-245D-4052-8B8C-7AA319BBD567} = {E3ADE8DE-5F3A-4E76-B22F-2C9435AACA06} {8D796D5A-9FC6-491B-906D-50B2AABAECF3} = {E3ADE8DE-5F3A-4E76-B22F-2C9435AACA06} {27E8A4B2-73DF-4C03-857B-2F529C3C49D6} = {E3ADE8DE-5F3A-4E76-B22F-2C9435AACA06} @@ -357,6 +356,7 @@ Global {FA1216E9-867C-4CAC-BFB5-0BDE8BC6CD2E} = {E3ADE8DE-5F3A-4E76-B22F-2C9435AACA06} {734B6128-C5E7-4746-B8B9-58B9798F9174} = {E3ADE8DE-5F3A-4E76-B22F-2C9435AACA06} {39B65A52-996E-4429-95DB-4731CD1EEAA9} = {78305463-1A38-4431-AFB0-0A0BE59D5A03} + {4EF33237-E340-4041-9A83-11B012D3A736} = {E3ADE8DE-5F3A-4E76-B22F-2C9435AACA06} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {9889BCDC-E6A6-448F-B01A-ECFD0FAF294C} diff --git a/src/WebSocket/WebSocketCore/WebSocketCore.csproj b/src/WebSocket/WebSocketCore/WebSocketCore.csproj index fe660eb7f..7816abde2 100644 --- a/src/WebSocket/WebSocketCore/WebSocketCore.csproj +++ b/src/WebSocket/WebSocketCore/WebSocketCore.csproj @@ -1,7 +1,7 @@ - netcoreapp3.1 + net6.0 Surging.WebSocketCore .NET CORE version based on websocket-sharp fanly