16 changed files with 825 additions and 33 deletions
@ -0,0 +1,168 @@ |
|||||
|
using LINGYUN.Abp.AspNetCore.HttpOverrides; |
||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer; |
||||
|
using Microsoft.AspNetCore.Builder; |
||||
|
using Microsoft.AspNetCore.DataProtection; |
||||
|
using Microsoft.AspNetCore.Hosting; |
||||
|
using Microsoft.AspNetCore.Routing; |
||||
|
using Microsoft.AspNetCore.Server.Kestrel.Core; |
||||
|
using Microsoft.Extensions.Caching.StackExchangeRedis; |
||||
|
using Microsoft.Extensions.Configuration; |
||||
|
using Microsoft.Extensions.DependencyInjection; |
||||
|
using Microsoft.Extensions.Hosting; |
||||
|
using Ocelot.Configuration.Repository; |
||||
|
using Ocelot.DependencyInjection; |
||||
|
using Ocelot.Extenssions; |
||||
|
using Ocelot.Middleware.Multiplexer; |
||||
|
using Ocelot.Provider.Polly; |
||||
|
using StackExchange.Redis; |
||||
|
using System; |
||||
|
using System.Text; |
||||
|
using Volo.Abp; |
||||
|
using Volo.Abp.AspNetCore; |
||||
|
using Volo.Abp.Autofac; |
||||
|
using Volo.Abp.Caching; |
||||
|
using Volo.Abp.Caching.StackExchangeRedis; |
||||
|
using Volo.Abp.Http.Client; |
||||
|
using Volo.Abp.Json; |
||||
|
using Volo.Abp.Modularity; |
||||
|
using Volo.Abp.Security.Encryption; |
||||
|
using Volo.Abp.VirtualFileSystem; |
||||
|
|
||||
|
namespace LINGYUN.Abp.ApiGateway |
||||
|
{ |
||||
|
[DependsOn( |
||||
|
typeof(AbpAutofacModule), |
||||
|
typeof(AbpHttpClientModule), |
||||
|
typeof(AbpCachingStackExchangeRedisModule), |
||||
|
typeof(AbpAspNetCoreModule), |
||||
|
typeof(AbpAspNetCoreHttpOverridesModule) |
||||
|
)] |
||||
|
public class AbpApiGatewayHostModule : AbpModule |
||||
|
{ |
||||
|
public override void PreConfigureServices(ServiceConfigurationContext context) |
||||
|
{ |
||||
|
context.Services.AddHttpClient("_AbpApiDefinitionClient"); |
||||
|
context.Services.AddSingleton<IFileConfigurationRepository, AbpApiDescriptionFileConfigurationRepository>(); |
||||
|
} |
||||
|
|
||||
|
public override void ConfigureServices(ServiceConfigurationContext context) |
||||
|
{ |
||||
|
var hostingEnvironment = context.Services.GetHostingEnvironment(); |
||||
|
var configuration = context.Services.GetConfiguration(); |
||||
|
|
||||
|
// 解决某些不支持类型的序列化
|
||||
|
Configure<AbpJsonOptions>(options => |
||||
|
{ |
||||
|
// See: https://docs.abp.io/en/abp/4.0/Migration-Guides/Abp-4_0#always-use-the-newtonsoft-json
|
||||
|
options.UseHybridSerializer = false; |
||||
|
}); |
||||
|
// 中文序列化的编码问题
|
||||
|
//Configure<AbpSystemTextJsonSerializerOptions>(options =>
|
||||
|
//{
|
||||
|
// options.JsonSerializerOptions.Encoder = JavaScriptEncoder.Create(UnicodeRanges.All);
|
||||
|
//});
|
||||
|
|
||||
|
Configure<AbpApiGatewayOptions>(configuration.GetSection("ApiGateway")); |
||||
|
|
||||
|
context.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) |
||||
|
.AddJwtBearer(options => |
||||
|
{ |
||||
|
options.Authority = configuration["AuthServer:Authority"]; |
||||
|
options.RequireHttpsMetadata = false; |
||||
|
options.Audience = configuration["AuthServer:ApiName"]; |
||||
|
}); |
||||
|
|
||||
|
Configure<AbpDistributedCacheOptions>(options => |
||||
|
{ |
||||
|
// 最好统一命名,不然某个缓存变动其他应用服务有例外发生
|
||||
|
options.KeyPrefix = "LINGYUN.Abp.Application"; |
||||
|
// 滑动过期30天
|
||||
|
options.GlobalCacheEntryOptions.SlidingExpiration = TimeSpan.FromDays(30); |
||||
|
// 绝对过期60天
|
||||
|
options.GlobalCacheEntryOptions.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(60); |
||||
|
}); |
||||
|
|
||||
|
Configure<RedisCacheOptions>(options => |
||||
|
{ |
||||
|
var redisConfig = ConfigurationOptions.Parse(options.Configuration); |
||||
|
options.ConfigurationOptions = redisConfig; |
||||
|
options.InstanceName = configuration["Redis:InstanceName"]; |
||||
|
}); |
||||
|
|
||||
|
// 加解密
|
||||
|
Configure<AbpStringEncryptionOptions>(options => |
||||
|
{ |
||||
|
var encryptionConfiguration = configuration.GetSection("Encryption"); |
||||
|
if (encryptionConfiguration.Exists()) |
||||
|
{ |
||||
|
options.DefaultPassPhrase = encryptionConfiguration["PassPhrase"] ?? options.DefaultPassPhrase; |
||||
|
options.DefaultSalt = encryptionConfiguration.GetSection("Salt").Exists() |
||||
|
? Encoding.ASCII.GetBytes(encryptionConfiguration["Salt"]) |
||||
|
: options.DefaultSalt; |
||||
|
options.InitVectorBytes = encryptionConfiguration.GetSection("InitVector").Exists() |
||||
|
? Encoding.ASCII.GetBytes(encryptionConfiguration["InitVector"]) |
||||
|
: options.InitVectorBytes; |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
Configure<AbpVirtualFileSystemOptions>(options => |
||||
|
{ |
||||
|
options.FileSets.AddEmbedded<AbpApiGatewayHostModule>(); |
||||
|
}); |
||||
|
|
||||
|
var mvcBuilder = context.Services.AddMvc(); |
||||
|
mvcBuilder.AddApplicationPart(typeof(AbpApiGatewayHostModule).Assembly); |
||||
|
|
||||
|
Configure<AbpEndpointRouterOptions>(options => |
||||
|
{ |
||||
|
options.EndpointConfigureActions.Add(endpointContext => |
||||
|
{ |
||||
|
endpointContext.Endpoints.MapControllerRoute("defaultWithArea", "{area}/{controller=Home}/{action=Index}/{id?}"); |
||||
|
endpointContext.Endpoints.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}"); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
if (!hostingEnvironment.IsDevelopment()) |
||||
|
{ |
||||
|
// Ssl证书
|
||||
|
var sslOptions = configuration.GetSection("App:SslOptions"); |
||||
|
if (sslOptions.Exists()) |
||||
|
{ |
||||
|
var fileName = sslOptions["FileName"]; |
||||
|
var password = sslOptions["Password"]; |
||||
|
Configure<KestrelServerOptions>(options => |
||||
|
{ |
||||
|
options.ConfigureEndpointDefaults(cfg => |
||||
|
{ |
||||
|
cfg.UseHttps(fileName, password); |
||||
|
}); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
var redis = ConnectionMultiplexer.Connect(configuration["Redis:Configuration"]); |
||||
|
context.Services |
||||
|
.AddDataProtection() |
||||
|
.PersistKeysToStackExchangeRedis(redis, "ApiGatewayHost-Protection-Keys"); |
||||
|
} |
||||
|
|
||||
|
context.Services |
||||
|
.AddOcelot() |
||||
|
.AddPolly() |
||||
|
.AddSingletonDefinedAggregator<AbpResponseMergeAggregator>(); |
||||
|
} |
||||
|
|
||||
|
public override void OnApplicationInitialization(ApplicationInitializationContext context) |
||||
|
{ |
||||
|
var app = context.GetApplicationBuilder(); |
||||
|
|
||||
|
app.UseForwardedHeaders(); |
||||
|
app.UseAuditing(); |
||||
|
app.UseStaticFiles(); |
||||
|
app.UseRouting(); |
||||
|
app.UseAuthentication(); |
||||
|
// 启用ws协议
|
||||
|
app.UseWebSockets(); |
||||
|
app.UseOcelot().Wait(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
using Ocelot.Configuration.File; |
||||
|
using System.Collections.Generic; |
||||
|
|
||||
|
namespace LINGYUN.Abp.ApiGateway |
||||
|
{ |
||||
|
public class AbpApiGatewayOptions |
||||
|
{ |
||||
|
public FileGlobalConfiguration GlobalConfiguration { get; set; } = new FileGlobalConfiguration(); |
||||
|
public List<string> AggrageRouteUrls { get; set; } = new List<string>(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,13 @@ |
|||||
|
FROM mcr.microsoft.com/dotnet/aspnet:5.0 |
||||
|
LABEL maintainer="colin.in@foxmail.com" |
||||
|
WORKDIR /app |
||||
|
|
||||
|
COPY . /app |
||||
|
|
||||
|
ENV TZ=Asia/Shanghai |
||||
|
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo '$TZ' > /etc/timezone |
||||
|
|
||||
|
EXPOSE 80/tcp |
||||
|
VOLUME [ "./app/Logs" ] |
||||
|
|
||||
|
ENTRYPOINT ["dotnet", "LINGYUN.ApiGateway.Host.dll"] |
||||
@ -0,0 +1,28 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk.Web"> |
||||
|
|
||||
|
<PropertyGroup> |
||||
|
<TargetFramework>net5.0</TargetFramework> |
||||
|
<RootNamespace>LINGYUN.Abp.ApiGateway</RootNamespace> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<PackageReference Include="Microsoft.AspNetCore.DataProtection.StackExchangeRedis" Version="5.0.*" /> |
||||
|
<PackageReference Include="Ocelot.Provider.Polly" Version="16.0.1" /> |
||||
|
<PackageReference Include="Serilog.AspNetCore" Version="3.4.0" /> |
||||
|
<PackageReference Include="Serilog.Enrichers.Assembly" Version="2.0.0" /> |
||||
|
<PackageReference Include="Serilog.Enrichers.Process" Version="2.0.1" /> |
||||
|
<PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" /> |
||||
|
<PackageReference Include="Serilog.Settings.Configuration" Version="3.1.0" /> |
||||
|
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" /> |
||||
|
<PackageReference Include="Volo.Abp.AspNetCore.Authentication.JwtBearer" Version="4.3.0" /> |
||||
|
<PackageReference Include="Volo.Abp.Autofac" Version="4.3.0" /> |
||||
|
<PackageReference Include="Volo.Abp.AspNetCore" Version="4.3.0" /> |
||||
|
<PackageReference Include="Volo.Abp.Http.Client" Version="4.3.0" /> |
||||
|
<PackageReference Include="Volo.Abp.Caching.StackExchangeRedis" Version="4.3.0" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<ProjectReference Include="..\..\..\modules\common\LINGYUN.Abp.AspNetCore.HttpOverrides\LINGYUN.Abp.AspNetCore.HttpOverrides.csproj" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
</Project> |
||||
@ -0,0 +1,6 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> |
||||
|
<PropertyGroup> |
||||
|
<ActiveDebugProfile>IIS Express</ActiveDebugProfile> |
||||
|
</PropertyGroup> |
||||
|
</Project> |
||||
@ -0,0 +1,185 @@ |
|||||
|
using LINGYUN.Abp.ApiGateway; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using Ocelot.Configuration.File; |
||||
|
using Ocelot.Middleware.Multiplexer; |
||||
|
using Ocelot.Responses; |
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Net.Http; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.Http.Client; |
||||
|
using Volo.Abp.Http.Client.DynamicProxying; |
||||
|
using Volo.Abp.Http.Modeling; |
||||
|
|
||||
|
namespace Ocelot.Configuration.Repository |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 通过定义的RemoteServices来寻找abp端点定义的服务列表
|
||||
|
/// 在AbpApiGatewayOptions.AggrageRouteUrls中定义聚合的url
|
||||
|
/// 可以在发现相同端点后加入到聚合路由,聚合路由下游端点会自动加入聚合标识,以发现的聚合数量作为前缀
|
||||
|
/// 例如/api/abp/api-definition、/api/abp/application-configuration
|
||||
|
/// 下游端点将变成/aggregate1/api/abp/api-definition、/aggregate1/api/abp/application-configuration
|
||||
|
/// 上游服务直接调用/api/abp/api-definition、/api/abp/application-configuration即可唤起路由聚合
|
||||
|
/// </summary>
|
||||
|
public class AbpApiDescriptionFileConfigurationRepository : IFileConfigurationRepository |
||||
|
{ |
||||
|
private readonly AbpRemoteServiceOptions _remoteServiceOptions; |
||||
|
private readonly AbpApiGatewayOptions _apiGatewayOptions; |
||||
|
|
||||
|
private readonly IApiDescriptionFinder _apiDescriptionFinder; |
||||
|
private readonly IHttpClientFactory _httpClientFactory; |
||||
|
public AbpApiDescriptionFileConfigurationRepository( |
||||
|
IHttpClientFactory httpClientFactory, |
||||
|
IApiDescriptionFinder apiDescriptionFinder, |
||||
|
IOptions<AbpApiGatewayOptions> apiGatewayOptions, |
||||
|
IOptions<AbpRemoteServiceOptions> remoteServiceOptions |
||||
|
) |
||||
|
{ |
||||
|
_httpClientFactory = httpClientFactory; |
||||
|
_apiDescriptionFinder = apiDescriptionFinder; |
||||
|
|
||||
|
_apiGatewayOptions = apiGatewayOptions.Value; |
||||
|
_remoteServiceOptions = remoteServiceOptions.Value; |
||||
|
} |
||||
|
public async Task<Response<FileConfiguration>> Get() |
||||
|
{ |
||||
|
var fileConfiguration = new FileConfiguration |
||||
|
{ |
||||
|
GlobalConfiguration = _apiGatewayOptions.GlobalConfiguration |
||||
|
}; |
||||
|
var apiDescriptionModels = await GetApiDescriptionsAsync(); |
||||
|
int foundAggrageRouteCount = 0; |
||||
|
foreach (var apiDescriptionModel in apiDescriptionModels) |
||||
|
{ |
||||
|
foreach (var moduleModel in apiDescriptionModel.Value.Modules) |
||||
|
{ |
||||
|
foreach (var controllerModel in moduleModel.Value.Controllers) |
||||
|
{ |
||||
|
foreach (var actionModel in controllerModel.Value.Actions) |
||||
|
{ |
||||
|
var action = actionModel.Value; |
||||
|
var downstreamUrl = action.Url.EnsureStartsWith('/'); |
||||
|
|
||||
|
// TODO: 多个相同的下游路由地址应组合为聚合路由
|
||||
|
|
||||
|
// TODO: 下游路由地址已添加
|
||||
|
var route = fileConfiguration.Routes |
||||
|
.Where(route => HasBeenAddedRoute(route, downstreamUrl)) |
||||
|
.LastOrDefault(); |
||||
|
string aggregateKey = ""; |
||||
|
|
||||
|
if (route != null) |
||||
|
{ |
||||
|
// TODO: 下游方法已添加
|
||||
|
if (route.UpstreamHttpMethod.Any(method => method.Equals(action.HttpMethod, StringComparison.CurrentCultureIgnoreCase))) |
||||
|
{ |
||||
|
if (_apiGatewayOptions.AggrageRouteUrls.Any(url => url.Equals(downstreamUrl))) |
||||
|
{ |
||||
|
foundAggrageRouteCount++; |
||||
|
var aggregateRoute = fileConfiguration.Aggregates |
||||
|
.Where(route => HasBeenAddedAggregateRoute(route, downstreamUrl)) |
||||
|
.FirstOrDefault(); |
||||
|
if (aggregateRoute == null) |
||||
|
{ |
||||
|
aggregateRoute = new FileAggregateRoute |
||||
|
{ |
||||
|
RouteIsCaseSensitive = false, |
||||
|
// TODO: 可实现自定义的聚合提供装置
|
||||
|
Aggregator = nameof(AbpResponseMergeAggregator), |
||||
|
UpstreamPathTemplate = downstreamUrl, |
||||
|
RouteKeys = new List<string>() |
||||
|
}; |
||||
|
fileConfiguration.Aggregates.Add(aggregateRoute); |
||||
|
} |
||||
|
if (route.Key.IsNullOrWhiteSpace()) |
||||
|
{ |
||||
|
route.Key = $"aggregate{foundAggrageRouteCount}"; |
||||
|
route.UpstreamPathTemplate = $"/{route.Key}{route.UpstreamPathTemplate}"; |
||||
|
aggregateRoute.RouteKeys.Add(route.Key); |
||||
|
foundAggrageRouteCount++; |
||||
|
} |
||||
|
aggregateKey = $"aggregate{foundAggrageRouteCount}"; |
||||
|
aggregateRoute.RouteKeys.Add(aggregateKey); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
continue; |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
route.UpstreamHttpMethod.Add(action.HttpMethod); |
||||
|
continue; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
var newRoute = new FileRoute |
||||
|
{ |
||||
|
Key = aggregateKey, |
||||
|
UpstreamPathTemplate = (aggregateKey + downstreamUrl).EnsureStartsWith('/'), |
||||
|
DownstreamPathTemplate = downstreamUrl, |
||||
|
DangerousAcceptAnyServerCertificateValidator = false |
||||
|
}; |
||||
|
|
||||
|
var baseUrl = apiDescriptionModel.Key; |
||||
|
baseUrl = baseUrl.StartsWith("http://") ? baseUrl[7..] : baseUrl; |
||||
|
baseUrl = baseUrl.StartsWith("https://") ? baseUrl[8..] : baseUrl; |
||||
|
baseUrl = baseUrl.EndsWith("/") ? baseUrl[0..^1] : baseUrl; |
||||
|
|
||||
|
var addresses = baseUrl.Split(":"); |
||||
|
var hostAndPort = new FileHostAndPort |
||||
|
{ |
||||
|
Host = addresses[0] |
||||
|
}; |
||||
|
if (addresses.Length == 2) |
||||
|
{ |
||||
|
hostAndPort.Port = int.Parse(addresses[1]); |
||||
|
} |
||||
|
newRoute.DownstreamHostAndPorts.Add(hostAndPort); |
||||
|
newRoute.UpstreamHttpMethod.Add(action.HttpMethod); |
||||
|
|
||||
|
// abp api版本号支持通过query发送
|
||||
|
//newRoute.DownstreamHttpVersion = action.SupportedVersions.Last();
|
||||
|
|
||||
|
fileConfiguration.Routes.Add(newRoute); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return new OkResponse<FileConfiguration>(fileConfiguration); |
||||
|
} |
||||
|
|
||||
|
private static bool HasBeenAddedRoute(FileRoute route, string downstreamUrl) |
||||
|
{ |
||||
|
return route.DownstreamPathTemplate.Equals(downstreamUrl, StringComparison.CurrentCultureIgnoreCase); |
||||
|
} |
||||
|
|
||||
|
private static bool HasBeenAddedAggregateRoute(FileAggregateRoute route, string upstreamUrl) |
||||
|
{ |
||||
|
return route.UpstreamPathTemplate.Equals(upstreamUrl, StringComparison.CurrentCultureIgnoreCase); |
||||
|
} |
||||
|
|
||||
|
public async Task<Response> Set(FileConfiguration fileConfiguration) |
||||
|
{ |
||||
|
return await Task.FromResult(new OkResponse<FileConfiguration>(fileConfiguration)); |
||||
|
} |
||||
|
|
||||
|
protected virtual async Task<Dictionary<string, ApplicationApiDescriptionModel>> GetApiDescriptionsAsync() |
||||
|
{ |
||||
|
var client = _httpClientFactory.CreateClient("_AbpApiDefinitionClient"); |
||||
|
|
||||
|
var apiDescriptionModels = new Dictionary<string, ApplicationApiDescriptionModel>(); |
||||
|
|
||||
|
foreach (var remoteService in _remoteServiceOptions.RemoteServices) |
||||
|
{ |
||||
|
var model = await _apiDescriptionFinder.GetApiDescriptionAsync(client, remoteService.Value.BaseUrl); |
||||
|
|
||||
|
apiDescriptionModels.Add(remoteService.Value.BaseUrl, model); |
||||
|
} |
||||
|
|
||||
|
return apiDescriptionModels; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,123 @@ |
|||||
|
using Microsoft.AspNetCore.Builder; |
||||
|
using Microsoft.AspNetCore.Hosting; |
||||
|
using Microsoft.Extensions.DependencyInjection; |
||||
|
using Ocelot.Configuration; |
||||
|
using Ocelot.Configuration.Creator; |
||||
|
using Ocelot.Configuration.Repository; |
||||
|
using Ocelot.DependencyInjection; |
||||
|
using Ocelot.Logging; |
||||
|
using Ocelot.Middleware; |
||||
|
using Ocelot.Responses; |
||||
|
using System; |
||||
|
using System.Diagnostics; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Ocelot.Extenssions |
||||
|
{ |
||||
|
public static class OcelotMiddlewareExtensions |
||||
|
{ |
||||
|
public static async Task<IApplicationBuilder> UseOcelot(this IApplicationBuilder builder) |
||||
|
{ |
||||
|
await builder.UseOcelot(new OcelotPipelineConfiguration()); |
||||
|
return builder; |
||||
|
} |
||||
|
|
||||
|
public static async Task<IApplicationBuilder> UseOcelot(this IApplicationBuilder builder, Action<OcelotPipelineConfiguration> pipelineConfiguration) |
||||
|
{ |
||||
|
var config = new OcelotPipelineConfiguration(); |
||||
|
pipelineConfiguration?.Invoke(config); |
||||
|
return await builder.UseOcelot(config); |
||||
|
} |
||||
|
|
||||
|
public static async Task<IApplicationBuilder> UseOcelot(this IApplicationBuilder builder, OcelotPipelineConfiguration pipelineConfiguration) |
||||
|
{ |
||||
|
var configuration = await CreateConfiguration(builder); |
||||
|
|
||||
|
ConfigureDiagnosticListener(builder); |
||||
|
|
||||
|
return CreateOcelotPipeline(builder, pipelineConfiguration); |
||||
|
} |
||||
|
|
||||
|
public static Task<IApplicationBuilder> UseOcelot(this IApplicationBuilder app, Action<IApplicationBuilder, OcelotPipelineConfiguration> builderAction) |
||||
|
=> UseOcelot(app, builderAction, new OcelotPipelineConfiguration()); |
||||
|
|
||||
|
public static async Task<IApplicationBuilder> UseOcelot(this IApplicationBuilder app, Action<IApplicationBuilder, OcelotPipelineConfiguration> builderAction, OcelotPipelineConfiguration configuration) |
||||
|
{ |
||||
|
await CreateConfiguration(app); |
||||
|
|
||||
|
ConfigureDiagnosticListener(app); |
||||
|
|
||||
|
builderAction?.Invoke(app, configuration ?? new OcelotPipelineConfiguration()); |
||||
|
|
||||
|
app.Properties["analysis.NextMiddlewareName"] = "TransitionToOcelotMiddleware"; |
||||
|
|
||||
|
return app; |
||||
|
} |
||||
|
|
||||
|
private static IApplicationBuilder CreateOcelotPipeline(IApplicationBuilder builder, OcelotPipelineConfiguration pipelineConfiguration) |
||||
|
{ |
||||
|
builder.BuildOcelotPipeline(pipelineConfiguration); |
||||
|
|
||||
|
/* |
||||
|
inject first delegate into first piece of asp.net middleware..maybe not like this |
||||
|
then because we are updating the http context in ocelot it comes out correct for |
||||
|
rest of asp.net.. |
||||
|
*/ |
||||
|
|
||||
|
builder.Properties["analysis.NextMiddlewareName"] = "TransitionToOcelotMiddleware"; |
||||
|
|
||||
|
return builder; |
||||
|
} |
||||
|
private static async Task<IInternalConfiguration> CreateConfiguration(IApplicationBuilder builder) |
||||
|
{ |
||||
|
var fileConfigRepo = builder.ApplicationServices.GetRequiredService<IFileConfigurationRepository>(); |
||||
|
var fileConfig = await fileConfigRepo.Get(); |
||||
|
// IOptionsMonitor<FileConfiguration> fileConfig = builder.ApplicationServices.GetService<IOptionsMonitor<FileConfiguration>>();
|
||||
|
IInternalConfigurationCreator internalConfigCreator = builder.ApplicationServices.GetService<IInternalConfigurationCreator>(); |
||||
|
Response<IInternalConfiguration> response = await internalConfigCreator.Create(fileConfig.Data); |
||||
|
if (response.IsError) |
||||
|
{ |
||||
|
ThrowToStopOcelotStarting(response); |
||||
|
} |
||||
|
IInternalConfigurationRepository internalConfigRepo = builder.ApplicationServices.GetService<IInternalConfigurationRepository>(); |
||||
|
internalConfigRepo.AddOrReplace(response.Data); |
||||
|
return GetOcelotConfigAndReturn(internalConfigRepo); |
||||
|
} |
||||
|
|
||||
|
private static bool AdministrationApiInUse(IAdministrationPath adminPath) |
||||
|
{ |
||||
|
return adminPath != null; |
||||
|
} |
||||
|
|
||||
|
private static bool IsError(Response response) |
||||
|
{ |
||||
|
return response == null || response.IsError; |
||||
|
} |
||||
|
|
||||
|
private static IInternalConfiguration GetOcelotConfigAndReturn(IInternalConfigurationRepository provider) |
||||
|
{ |
||||
|
var ocelotConfiguration = provider.Get(); |
||||
|
|
||||
|
if (ocelotConfiguration?.Data == null || ocelotConfiguration.IsError) |
||||
|
{ |
||||
|
ThrowToStopOcelotStarting(ocelotConfiguration); |
||||
|
} |
||||
|
|
||||
|
return ocelotConfiguration.Data; |
||||
|
} |
||||
|
|
||||
|
private static void ThrowToStopOcelotStarting(Response config) |
||||
|
{ |
||||
|
throw new Exception($"Unable to start Ocelot, errors are: {string.Join(",", config.Errors.Select(x => x.ToString()))}"); |
||||
|
} |
||||
|
|
||||
|
private static void ConfigureDiagnosticListener(IApplicationBuilder builder) |
||||
|
{ |
||||
|
var env = builder.ApplicationServices.GetService<IWebHostEnvironment>(); |
||||
|
var listener = builder.ApplicationServices.GetService<OcelotDiagnosticListener>(); |
||||
|
var diagnosticListener = builder.ApplicationServices.GetService<DiagnosticListener>(); |
||||
|
diagnosticListener.SubscribeWithAdapter(listener); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,51 @@ |
|||||
|
using Microsoft.AspNetCore.Http; |
||||
|
using Newtonsoft.Json; |
||||
|
using Newtonsoft.Json.Linq; |
||||
|
using Ocelot.Multiplexer; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Net; |
||||
|
using System.Net.Http; |
||||
|
using System.Net.Http.Headers; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Ocelot.Middleware.Multiplexer |
||||
|
{ |
||||
|
public class AbpResponseMergeAggregator : IDefinedAggregator |
||||
|
{ |
||||
|
public async Task<DownstreamResponse> Aggregate(List<HttpContext> responses) |
||||
|
{ |
||||
|
return await MapAbpApiDefinitionAggregateContentAsync(responses); |
||||
|
} |
||||
|
|
||||
|
protected virtual async Task<DownstreamResponse> MapAbpApiDefinitionAggregateContentAsync(List<HttpContext> responses) |
||||
|
{ |
||||
|
JObject responseObject = null; |
||||
|
JsonMergeSettings mergeSetting = new JsonMergeSettings |
||||
|
{ |
||||
|
MergeArrayHandling = MergeArrayHandling.Union, |
||||
|
PropertyNameComparison = System.StringComparison.CurrentCultureIgnoreCase |
||||
|
}; |
||||
|
foreach (var httpResponse in responses) |
||||
|
{ |
||||
|
var content = await httpResponse.Items.DownstreamResponse().Content.ReadAsStringAsync(); |
||||
|
var contentObject = JsonConvert.DeserializeObject(content); |
||||
|
if (responseObject == null) |
||||
|
{ |
||||
|
responseObject = JObject.FromObject(contentObject); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
responseObject.Merge(contentObject, mergeSetting); |
||||
|
} |
||||
|
} |
||||
|
var stringContent = new StringContent(responseObject.ToString()) |
||||
|
{ |
||||
|
Headers = { ContentType = new MediaTypeHeaderValue("application/json") } |
||||
|
}; |
||||
|
stringContent.Headers.Add("_AbpErrorFormat", "true"); |
||||
|
return new DownstreamResponse(stringContent, HttpStatusCode.OK, |
||||
|
new List<KeyValuePair<string, IEnumerable<string>>>(), "OK"); |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,47 @@ |
|||||
|
using Microsoft.AspNetCore.Hosting; |
||||
|
using Microsoft.Extensions.Configuration; |
||||
|
using Microsoft.Extensions.Hosting; |
||||
|
using Serilog; |
||||
|
using System; |
||||
|
using System.IO; |
||||
|
|
||||
|
namespace LINGYUN.Abp.ApiGateway |
||||
|
{ |
||||
|
public class Program |
||||
|
{ |
||||
|
public static int Main(string[] args) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
var hostBuilder = CreateHostBuilder(args).Build(); |
||||
|
Log.Information("Starting web host."); |
||||
|
hostBuilder.Run(); |
||||
|
|
||||
|
return 0; |
||||
|
} |
||||
|
catch (Exception ex) |
||||
|
{ |
||||
|
Log.Fatal(ex, "Host terminated unexpectedly!"); |
||||
|
return 1; |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
Log.CloseAndFlush(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
internal static IHostBuilder CreateHostBuilder(string[] args) => |
||||
|
Host.CreateDefaultBuilder(args) |
||||
|
.ConfigureWebHostDefaults(webBuilder => |
||||
|
{ |
||||
|
webBuilder |
||||
|
.UseKestrel() |
||||
|
.UseStartup<Startup>(); |
||||
|
}) |
||||
|
.UseSerilog((context, provider, config) => |
||||
|
{ |
||||
|
config.ReadFrom.Configuration(context.Configuration); |
||||
|
}) |
||||
|
.UseAutofac(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,20 @@ |
|||||
|
{ |
||||
|
"iisSettings": { |
||||
|
"windowsAuthentication": false, |
||||
|
"anonymousAuthentication": true, |
||||
|
"iisExpress": { |
||||
|
"applicationUrl": "http://localhost:54450/", |
||||
|
"sslPort": 0 |
||||
|
} |
||||
|
}, |
||||
|
"profiles": { |
||||
|
"LINGYUN.Abp.ApiGateway.Host": { |
||||
|
"commandName": "Project", |
||||
|
"launchBrowser": false, |
||||
|
"environmentVariables": { |
||||
|
"ASPNETCORE_ENVIRONMENT": "Development" |
||||
|
}, |
||||
|
"applicationUrl": "http://localhost:30000" |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,18 @@ |
|||||
|
using Microsoft.AspNetCore.Builder; |
||||
|
using Microsoft.Extensions.DependencyInjection; |
||||
|
|
||||
|
namespace LINGYUN.Abp.ApiGateway |
||||
|
{ |
||||
|
public class Startup |
||||
|
{ |
||||
|
public void ConfigureServices(IServiceCollection services) |
||||
|
{ |
||||
|
services.AddApplication<AbpApiGatewayHostModule>(); |
||||
|
} |
||||
|
|
||||
|
public void Configure(IApplicationBuilder app) |
||||
|
{ |
||||
|
app.InitializeApplication(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue