From 3d16d5580b9d48c818cec2b144e5f694c28e307b Mon Sep 17 00:00:00 2001 From: Abdulatif Rasulov Date: Wed, 16 Oct 2024 17:07:14 +0500 Subject: [PATCH] 1.On BuilderExtensions.cs AddSwaggerEndPoints method InvalidOperationException throwing block refactored using IEndPointValidator.cs 2. created custom OnChange instead of IOptionsMonitor.OnChange to monipulate with Options which added Programmatically and don't effect to config options. Both works well --- .../ServiceCollectionExtensions.cs | 7 ++ .../Middleware/BuilderExtensions.cs | 27 ++-- .../ConsulSwaggerEndpointProvider.cs | 2 +- .../ISwaggerEndPointProvider.cs | 0 .../SwaggerEndPointProvider.cs | 0 .../ConsulEndPointValidator.cs | 18 +++ .../EndPointValidators/EndPointValidator.cs | 24 ++++ .../EndPointValidators/IEndPointValidator.cs | 16 +++ .../ConsulSwaggerEndpointsMonitor.cs | 79 ++++++++++++ .../ISwaggerEndpointsMonitor.cs | 16 +++ .../SwaggerEndpointsMonitor.cs | 52 ++++++++ .../ConsulEndpointOptionsMonitor.cs | 116 ++++++++++++++++++ .../IConsulEndpointOptionsMonitor.cs | 21 ++++ .../DownstreamSwaggerDocsRepository.cs | 0 .../IDownstreamSwaggerDocsRepository.cs | 0 .../ConsulServiceDisvovery.cs | 73 ++++++++--- 16 files changed, 418 insertions(+), 33 deletions(-) rename src/MMLib.SwaggerForOcelot/Repositories/{ => EndPointProviders}/ConsulSwaggerEndpointProvider.cs (96%) rename src/MMLib.SwaggerForOcelot/Repositories/{ => EndPointProviders}/ISwaggerEndPointProvider.cs (100%) rename src/MMLib.SwaggerForOcelot/Repositories/{ => EndPointProviders}/SwaggerEndPointProvider.cs (100%) create mode 100644 src/MMLib.SwaggerForOcelot/Repositories/EndPointValidators/ConsulEndPointValidator.cs create mode 100644 src/MMLib.SwaggerForOcelot/Repositories/EndPointValidators/EndPointValidator.cs create mode 100644 src/MMLib.SwaggerForOcelot/Repositories/EndPointValidators/IEndPointValidator.cs create mode 100644 src/MMLib.SwaggerForOcelot/Repositories/EndPointsMonitor/ConsulSwaggerEndpointsMonitor.cs create mode 100644 src/MMLib.SwaggerForOcelot/Repositories/EndPointsMonitor/ISwaggerEndpointsMonitor.cs create mode 100644 src/MMLib.SwaggerForOcelot/Repositories/EndPointsMonitor/SwaggerEndpointsMonitor.cs create mode 100644 src/MMLib.SwaggerForOcelot/Repositories/OptionsMonitor/ConsulEndpointOptionsMonitor.cs create mode 100644 src/MMLib.SwaggerForOcelot/Repositories/OptionsMonitor/IConsulEndpointOptionsMonitor.cs rename src/MMLib.SwaggerForOcelot/Repositories/{ => SwaggerDocs}/DownstreamSwaggerDocsRepository.cs (100%) rename src/MMLib.SwaggerForOcelot/Repositories/{ => SwaggerDocs}/IDownstreamSwaggerDocsRepository.cs (100%) diff --git a/src/MMLib.SwaggerForOcelot/DependencyInjection/ServiceCollectionExtensions.cs b/src/MMLib.SwaggerForOcelot/DependencyInjection/ServiceCollectionExtensions.cs index 76a9300..9084d0b 100644 --- a/src/MMLib.SwaggerForOcelot/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/MMLib.SwaggerForOcelot/DependencyInjection/ServiceCollectionExtensions.cs @@ -12,6 +12,7 @@ using MMLib.SwaggerForOcelot.Aggregates; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; +using MMLib.SwaggerForOcelot.Repositories.EndPointValidators; using MMLib.SwaggerForOcelot.ServiceDiscovery.ConsulServiceDiscoveries; using Ocelot.Configuration; using Ocelot.Configuration.Creator; @@ -47,6 +48,8 @@ public static IServiceCollection AddSwaggerForOcelot( { services .AddSingleton() + .AddSingleton() + .AddSingleton() .AddTransient() .AddTransient() .AddTransient() @@ -62,7 +65,11 @@ public static IServiceCollection AddSwaggerForOcelot( if (conf?.Type is ("Consul" or "PollConsul")) { services.AddConsulClient(conf); + + services.AddSingleton(); + services.AddSingleton(); services.AddTransient(); + services.AddSingleton(); } services.AddHttpClient(IgnoreSslCertificate, c => diff --git a/src/MMLib.SwaggerForOcelot/Middleware/BuilderExtensions.cs b/src/MMLib.SwaggerForOcelot/Middleware/BuilderExtensions.cs index f5560a0..926defb 100644 --- a/src/MMLib.SwaggerForOcelot/Middleware/BuilderExtensions.cs +++ b/src/MMLib.SwaggerForOcelot/Middleware/BuilderExtensions.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using MMLib.SwaggerForOcelot.Repositories; +using MMLib.SwaggerForOcelot.Repositories.EndPointValidators; namespace Microsoft.AspNetCore.Builder { @@ -41,7 +42,7 @@ public static IApplicationBuilder UseSwaggerForOcelotUI( .ApplicationServices.GetService().GetAll(); ChangeDetection(app, c, options); - AddSwaggerEndPoints(c, endPoints, options.DownstreamSwaggerEndPointBasePath); + AddSwaggerEndPoints(app, c, endPoints, options.DownstreamSwaggerEndPointBasePath); }); return app; @@ -50,14 +51,14 @@ public static IApplicationBuilder UseSwaggerForOcelotUI( private static void ChangeDetection(IApplicationBuilder app, SwaggerUIOptions c, SwaggerForOcelotUIOptions options) { - IOptionsMonitor> endpointsChangeMonitor = - app.ApplicationServices.GetService>>(); + var endpointsChangeMonitor = + app.ApplicationServices.GetService(); - endpointsChangeMonitor.OnChange((newEndpoints) => + endpointsChangeMonitor.OptionsChanged += (s, newEndpoints) => { c.ConfigObject.Urls = null; - AddSwaggerEndPoints(c, newEndpoints, options.DownstreamSwaggerEndPointBasePath); - }); + AddSwaggerEndPoints(app, c, newEndpoints, options.DownstreamSwaggerEndPointBasePath); + }; } /// @@ -79,25 +80,23 @@ private static void UseSwaggerForOcelot(IApplicationBuilder app, SwaggerForOcelo => app.Map(options.PathToSwaggerGenerator, builder => builder.UseMiddleware(options)); - private static void AddSwaggerEndPoints( - SwaggerUIOptions c, + private static void AddSwaggerEndPoints(IApplicationBuilder app, + SwaggerUIOptions swaggerOptions, IReadOnlyList endPoints, string basePath) { static string GetDescription(SwaggerEndPointConfig config) => config.IsGatewayItSelf ? config.Name : $"{config.Name} - {config.Version}"; - // if (endPoints is null || endPoints.Count == 0) - // { - // throw new InvalidOperationException( - // $"{SwaggerEndPointOptions.ConfigurationSectionName} configuration section is missing or empty."); - // } + var validator = app.ApplicationServices.GetRequiredService(); + validator.Validate(endPoints); foreach (SwaggerEndPointOptions endPoint in endPoints) { foreach (SwaggerEndPointConfig config in endPoint.Config) { - c.SwaggerEndpoint($"{basePath}/{config.Version}/{endPoint.KeyToPath}", GetDescription(config)); + swaggerOptions.SwaggerEndpoint($"{basePath}/{config.Version}/{endPoint.KeyToPath}", + GetDescription(config)); } } } diff --git a/src/MMLib.SwaggerForOcelot/Repositories/ConsulSwaggerEndpointProvider.cs b/src/MMLib.SwaggerForOcelot/Repositories/EndPointProviders/ConsulSwaggerEndpointProvider.cs similarity index 96% rename from src/MMLib.SwaggerForOcelot/Repositories/ConsulSwaggerEndpointProvider.cs rename to src/MMLib.SwaggerForOcelot/Repositories/EndPointProviders/ConsulSwaggerEndpointProvider.cs index 57c0c12..cbbc50e 100644 --- a/src/MMLib.SwaggerForOcelot/Repositories/ConsulSwaggerEndpointProvider.cs +++ b/src/MMLib.SwaggerForOcelot/Repositories/EndPointProviders/ConsulSwaggerEndpointProvider.cs @@ -14,7 +14,7 @@ public class ConsulSwaggerEndpointProvider : ISwaggerEndPointProvider /// /// /// - public IConsulServiceDiscovery _service { get; set; } + private IConsulServiceDiscovery _service; /// /// diff --git a/src/MMLib.SwaggerForOcelot/Repositories/ISwaggerEndPointProvider.cs b/src/MMLib.SwaggerForOcelot/Repositories/EndPointProviders/ISwaggerEndPointProvider.cs similarity index 100% rename from src/MMLib.SwaggerForOcelot/Repositories/ISwaggerEndPointProvider.cs rename to src/MMLib.SwaggerForOcelot/Repositories/EndPointProviders/ISwaggerEndPointProvider.cs diff --git a/src/MMLib.SwaggerForOcelot/Repositories/SwaggerEndPointProvider.cs b/src/MMLib.SwaggerForOcelot/Repositories/EndPointProviders/SwaggerEndPointProvider.cs similarity index 100% rename from src/MMLib.SwaggerForOcelot/Repositories/SwaggerEndPointProvider.cs rename to src/MMLib.SwaggerForOcelot/Repositories/EndPointProviders/SwaggerEndPointProvider.cs diff --git a/src/MMLib.SwaggerForOcelot/Repositories/EndPointValidators/ConsulEndPointValidator.cs b/src/MMLib.SwaggerForOcelot/Repositories/EndPointValidators/ConsulEndPointValidator.cs new file mode 100644 index 0000000..912a76c --- /dev/null +++ b/src/MMLib.SwaggerForOcelot/Repositories/EndPointValidators/ConsulEndPointValidator.cs @@ -0,0 +1,18 @@ +using MMLib.SwaggerForOcelot.Configuration; +using System.Collections.Generic; + +namespace MMLib.SwaggerForOcelot.Repositories.EndPointValidators; + +/// +/// +/// +public class ConsulEndPointValidator : IEndPointValidator +{ + /// + /// + /// + /// + public void Validate(IReadOnlyList endPoints) + { + } +} diff --git a/src/MMLib.SwaggerForOcelot/Repositories/EndPointValidators/EndPointValidator.cs b/src/MMLib.SwaggerForOcelot/Repositories/EndPointValidators/EndPointValidator.cs new file mode 100644 index 0000000..e1669b9 --- /dev/null +++ b/src/MMLib.SwaggerForOcelot/Repositories/EndPointValidators/EndPointValidator.cs @@ -0,0 +1,24 @@ +using MMLib.SwaggerForOcelot.Configuration; +using System; +using System.Collections.Generic; + +namespace MMLib.SwaggerForOcelot.Repositories.EndPointValidators; + +/// +/// +/// +public class EndPointValidator : IEndPointValidator +{ + /// + /// + /// + /// + public void Validate(IReadOnlyList endPoints) + { + if (endPoints is null || endPoints.Count == 0) + { + throw new InvalidOperationException( + $"{SwaggerEndPointOptions.ConfigurationSectionName} configuration section is missing or empty."); + } + } +} diff --git a/src/MMLib.SwaggerForOcelot/Repositories/EndPointValidators/IEndPointValidator.cs b/src/MMLib.SwaggerForOcelot/Repositories/EndPointValidators/IEndPointValidator.cs new file mode 100644 index 0000000..22a6817 --- /dev/null +++ b/src/MMLib.SwaggerForOcelot/Repositories/EndPointValidators/IEndPointValidator.cs @@ -0,0 +1,16 @@ +using MMLib.SwaggerForOcelot.Configuration; +using System.Collections.Generic; + +namespace MMLib.SwaggerForOcelot.Repositories.EndPointValidators; + +/// +/// +/// +public interface IEndPointValidator +{ + /// + /// + /// + /// + void Validate(IReadOnlyList endPoints); +} diff --git a/src/MMLib.SwaggerForOcelot/Repositories/EndPointsMonitor/ConsulSwaggerEndpointsMonitor.cs b/src/MMLib.SwaggerForOcelot/Repositories/EndPointsMonitor/ConsulSwaggerEndpointsMonitor.cs new file mode 100644 index 0000000..8605453 --- /dev/null +++ b/src/MMLib.SwaggerForOcelot/Repositories/EndPointsMonitor/ConsulSwaggerEndpointsMonitor.cs @@ -0,0 +1,79 @@ +#nullable enable +using Microsoft.Extensions.Options; +using MMLib.SwaggerForOcelot.Configuration; +using Swashbuckle.AspNetCore.Swagger; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MMLib.SwaggerForOcelot.Repositories; + +/// +/// +/// +public class ConsulSwaggerEndpointsMonitor : ISwaggerEndpointsMonitor +{ + /// + /// + /// + private readonly IOptionsMonitor> _optionsMonitor; + + /// + /// + /// + private readonly IConsulEndpointOptionsMonitor _consulOptionsMonitor; + + /// + /// + /// + public event EventHandler> OptionsChanged; + + /// + /// + /// + /// + /// + public ConsulSwaggerEndpointsMonitor(IOptionsMonitor> optionsMonitor, + IConsulEndpointOptionsMonitor consulOptionsMonitor) + { + _optionsMonitor = optionsMonitor; + _consulOptionsMonitor = consulOptionsMonitor; + _optionsMonitor.OnChange(ConfigChanged); + _consulOptionsMonitor.OptionsChanged +=(s,e) => ConfigChanged(e); + } + + /// + /// + /// + /// + /// + private void ConfigChanged(List configOptions) + { + var options = ConcatOptions(_optionsMonitor.CurrentValue, _consulOptionsMonitor.CurrentValue); + CallOptionsChanged(options); + } + + /// + /// + /// + /// + /// + /// + protected virtual List ConcatOptions(List configOptions, + List localOptions) + { + return configOptions + .Concat(localOptions) + .DistinctBy(s => s.Key) + .ToList(); + } + + /// + /// + /// + /// + protected virtual void CallOptionsChanged(List options) + { + OptionsChanged?.Invoke(this, options); + } +} diff --git a/src/MMLib.SwaggerForOcelot/Repositories/EndPointsMonitor/ISwaggerEndpointsMonitor.cs b/src/MMLib.SwaggerForOcelot/Repositories/EndPointsMonitor/ISwaggerEndpointsMonitor.cs new file mode 100644 index 0000000..ee34f3f --- /dev/null +++ b/src/MMLib.SwaggerForOcelot/Repositories/EndPointsMonitor/ISwaggerEndpointsMonitor.cs @@ -0,0 +1,16 @@ +using MMLib.SwaggerForOcelot.Configuration; +using System; +using System.Collections.Generic; + +namespace MMLib.SwaggerForOcelot.Repositories; + +/// +/// +/// +public interface ISwaggerEndpointsMonitor +{ + /// + /// + /// + event EventHandler> OptionsChanged; +} diff --git a/src/MMLib.SwaggerForOcelot/Repositories/EndPointsMonitor/SwaggerEndpointsMonitor.cs b/src/MMLib.SwaggerForOcelot/Repositories/EndPointsMonitor/SwaggerEndpointsMonitor.cs new file mode 100644 index 0000000..08cb277 --- /dev/null +++ b/src/MMLib.SwaggerForOcelot/Repositories/EndPointsMonitor/SwaggerEndpointsMonitor.cs @@ -0,0 +1,52 @@ +#nullable enable +using Microsoft.Extensions.Options; +using MMLib.SwaggerForOcelot.Configuration; +using System; +using System.Collections.Generic; + +namespace MMLib.SwaggerForOcelot.Repositories; + +/// +/// +/// +public class SwaggerEndpointsMonitor : ISwaggerEndpointsMonitor +{ + /// + /// + /// + private readonly IOptionsMonitor> _optionsMonitor; + + /// + /// + /// + public event EventHandler> OptionsChanged; + + /// + /// + /// + /// + public SwaggerEndpointsMonitor(IOptionsMonitor> optionsMonitor) + { + _optionsMonitor = optionsMonitor; + _optionsMonitor.OnChange(ConfigChanged); + } + + /// + /// + /// + /// + /// + private void ConfigChanged(List configOptions) + { + CallOptionsChanged(_optionsMonitor.CurrentValue); + } + + /// + /// + /// + /// + protected virtual void CallOptionsChanged(List options) + { + OptionsChanged?.Invoke(this, options); + } +} diff --git a/src/MMLib.SwaggerForOcelot/Repositories/OptionsMonitor/ConsulEndpointOptionsMonitor.cs b/src/MMLib.SwaggerForOcelot/Repositories/OptionsMonitor/ConsulEndpointOptionsMonitor.cs new file mode 100644 index 0000000..a97eea7 --- /dev/null +++ b/src/MMLib.SwaggerForOcelot/Repositories/OptionsMonitor/ConsulEndpointOptionsMonitor.cs @@ -0,0 +1,116 @@ +using MMLib.SwaggerForOcelot.Configuration; +using MMLib.SwaggerForOcelot.ServiceDiscovery.ConsulServiceDiscoveries; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Timers; + +namespace MMLib.SwaggerForOcelot.Repositories; + +/// +/// +/// +public class ConsulEndpointOptionsMonitor : IConsulEndpointOptionsMonitor +{ + /// + /// + /// + private readonly IConsulServiceDiscovery _service; + + /// + /// + /// + private readonly Timer _timer; + + /// + /// + /// + public event EventHandler> OptionsChanged; + + /// + /// + /// + public List CurrentValue { get; private set; } = new(); + + /// + /// + /// + public ConsulEndpointOptionsMonitor(IConsulServiceDiscovery service) + { + _service = service; + _timer = new Timer(300); + _timer.Elapsed += (s, e) => TimerElapsed(); + _timer.Start(); + } + + /// + /// + /// + private void TimerElapsed() + { + try + { + TryGetConsulOptions(); + } + catch (Exception ex) + { + _timer.Start(); + } + } + + /// + /// + /// + private void TryGetConsulOptions() + { + _timer.Stop(); + + var services = _service + .GetServicesAsync() + .GetAwaiter() + .GetResult(); + + if (!IsOptionsChanged(services)) + { + _timer.Start(); + return; + } + + CurrentValue = services; + OptionsChanged?.Invoke(this, CurrentValue); + _timer.Start(); + } + + /// + /// + /// + /// + /// + private bool IsOptionsChanged(List services) + { + var newValues = services.ToList(); + var oldValues = CurrentValue.ToList(); + if (newValues.Count != oldValues.Count) + return true; + + if (oldValues.Any(a => newValues.All(c => c.Key != a.Key))) + return true; + + return oldValues.Any(a => IsConfigChanged(a, newValues)); + } + + /// + /// + /// + /// + /// + /// + private bool IsConfigChanged(SwaggerEndPointOptions endpoint, List newValues) + { + var newEndpoint = newValues.FirstOrDefault(f => f.Key == endpoint.Key); + if (newEndpoint is null) + return true; + + return endpoint.Config.Any(a => newEndpoint.Config.All(c => c.Name != a.Name || c.Version != a.Version)); + } +} diff --git a/src/MMLib.SwaggerForOcelot/Repositories/OptionsMonitor/IConsulEndpointOptionsMonitor.cs b/src/MMLib.SwaggerForOcelot/Repositories/OptionsMonitor/IConsulEndpointOptionsMonitor.cs new file mode 100644 index 0000000..8d5b0f5 --- /dev/null +++ b/src/MMLib.SwaggerForOcelot/Repositories/OptionsMonitor/IConsulEndpointOptionsMonitor.cs @@ -0,0 +1,21 @@ +using MMLib.SwaggerForOcelot.Configuration; +using System; +using System.Collections.Generic; + +namespace MMLib.SwaggerForOcelot.Repositories; + +/// +/// +/// +public interface IConsulEndpointOptionsMonitor +{ + /// + /// + /// + event EventHandler> OptionsChanged; + + /// + /// + /// + List CurrentValue { get; } +} diff --git a/src/MMLib.SwaggerForOcelot/Repositories/DownstreamSwaggerDocsRepository.cs b/src/MMLib.SwaggerForOcelot/Repositories/SwaggerDocs/DownstreamSwaggerDocsRepository.cs similarity index 100% rename from src/MMLib.SwaggerForOcelot/Repositories/DownstreamSwaggerDocsRepository.cs rename to src/MMLib.SwaggerForOcelot/Repositories/SwaggerDocs/DownstreamSwaggerDocsRepository.cs diff --git a/src/MMLib.SwaggerForOcelot/Repositories/IDownstreamSwaggerDocsRepository.cs b/src/MMLib.SwaggerForOcelot/Repositories/SwaggerDocs/IDownstreamSwaggerDocsRepository.cs similarity index 100% rename from src/MMLib.SwaggerForOcelot/Repositories/IDownstreamSwaggerDocsRepository.cs rename to src/MMLib.SwaggerForOcelot/Repositories/SwaggerDocs/IDownstreamSwaggerDocsRepository.cs diff --git a/src/MMLib.SwaggerForOcelot/ServiceDiscovery/ConsulServiceDiscoveries/ConsulServiceDisvovery.cs b/src/MMLib.SwaggerForOcelot/ServiceDiscovery/ConsulServiceDiscoveries/ConsulServiceDisvovery.cs index 0680259..e8fbb54 100644 --- a/src/MMLib.SwaggerForOcelot/ServiceDiscovery/ConsulServiceDiscoveries/ConsulServiceDisvovery.cs +++ b/src/MMLib.SwaggerForOcelot/ServiceDiscovery/ConsulServiceDiscoveries/ConsulServiceDisvovery.cs @@ -54,24 +54,61 @@ private async Task> GetConsulServices() { var services = await _consulClient.Agent.Services(); - var endpoints = services.Response - .Select(service => new SwaggerEndPointOptions - { - Key = service.Key, - TransformByOcelotConfig = false, - Config = service.Value.Meta - .Where(w => w.Key.StartsWith("swagger")) - .Select(swagger => new SwaggerEndPointConfig - { - Name = $"{service.Value.Service} API", - Version = swagger.Value, - Service = new SwaggerService - { - Name = service.Value.Service, Path = $"swagger/{swagger.Value}/swagger.json" - } - }).ToList() - }).ToList(); + return services.Response + .Select(s => ConvertToOption(s.Key, s.Value)) + .ToList(); + } + + /// + /// + /// + /// + /// + /// + private SwaggerEndPointOptions ConvertToOption(string key, AgentService service) + { + var option = new SwaggerEndPointOptions(); + option.Key = key; + option.TransformByOcelotConfig = false; + option.Config = service.Meta + .Where(w => w.Key.StartsWith("swagger")) + .Select(swagger => ConvertToConfig(swagger, service)) + .ToList(); + + if (option.Config.Count == 0) + option.Config.Add(DefaultConfig(service)); + + return option; + } + + /// + /// + /// + /// + /// + /// + private SwaggerEndPointConfig ConvertToConfig(KeyValuePair swagger, AgentService service) + { + var config = new SwaggerEndPointConfig(); + config.Name = $"{service.Service} API"; + config.Version = swagger.Value; + config.Service = new SwaggerService { Name = service.Service, Path = $"swagger/{swagger.Value}/swagger.json" }; + + return config; + } + + /// + /// + /// + /// + /// + private SwaggerEndPointConfig DefaultConfig(AgentService service) + { + var config = new SwaggerEndPointConfig(); + config.Name = $"{service.Service} API"; + config.Version = "v1"; + config.Service = new SwaggerService { Name = service.Service, Path = $"swagger/{config.Version}/swagger.json" }; - return endpoints; + return config; } }