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; } }