From 21e09beaa86ed1bd802a8d0c6c84956e9971d386 Mon Sep 17 00:00:00 2001 From: Sean McIlvenna Date: Fri, 20 Dec 2024 12:20:16 -0800 Subject: [PATCH 01/11] Modifications to BFF to gather swagger specs for proxy'd services and include their operations in the BFF's spec. TODO: Still need to include schemas from proxy'd services. --- .../Swagger/ServiceSpecAppender.cs | 97 +++++++++++++++++++ DotNet/LinkAdmin.BFF/LinkAdmin.BFF.csproj | 1 + DotNet/LinkAdmin.BFF/Program.cs | 4 +- .../Models/Configs/ServiceRegistry.cs | 1 + 4 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 DotNet/LinkAdmin.BFF/Application/Swagger/ServiceSpecAppender.cs diff --git a/DotNet/LinkAdmin.BFF/Application/Swagger/ServiceSpecAppender.cs b/DotNet/LinkAdmin.BFF/Application/Swagger/ServiceSpecAppender.cs new file mode 100644 index 000000000..587597a93 --- /dev/null +++ b/DotNet/LinkAdmin.BFF/Application/Swagger/ServiceSpecAppender.cs @@ -0,0 +1,97 @@ +using LantanaGroup.Link.Shared.Application.Models.Configs; +using Microsoft.Extensions.Options; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Readers; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace LantanaGroup.Link.LinkAdmin.BFF.Application.Swagger; + +public class ServiceSpecAppender( + IOptions serviceRegistry, + HttpClient httpClient, + ILogger logger) : IDocumentFilter +{ + private static readonly Dictionary _responseCache = new(); + + private async Task GetServiceSpec(string swaggerSpecUrl) + { + if (!_responseCache.TryGetValue(swaggerSpecUrl, out var response)) + { + response = await httpClient.GetStringAsync(swaggerSpecUrl); + _responseCache[swaggerSpecUrl] = response; + } + + var openApiDocument = new OpenApiStringReader().Read(response, out var diagnostic); + return openApiDocument; + } + + private static string GetDotNetSwaggerSpecUrl(string serviceUrl) + { + return serviceUrl.TrimEnd('/') + "/swagger/v1/swagger.json"; + } + + private static string GetJavaSwaggerSpecUrl(string serviceUrl) + { + return serviceUrl.TrimEnd('/') + "/v3/api-docs"; + } + + private async Task AddServiceSpec(OpenApiDocument swaggerDoc, string swaggerSpecUrl) + { + try + { + var serviceSpec = await GetServiceSpec(swaggerSpecUrl); + foreach (var path in serviceSpec.Paths) + { + // Assumes that the proxy path of the service is the same as the paths in the service spec + swaggerDoc.Paths.Add(path.Key, path.Value); + } + } + catch (Exception ex) + { + logger.LogWarning(ex, "Failed to add service spec to swagger doc"); + } + } + + public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) + { + var tasks = new List(); + + if (!string.IsNullOrEmpty(serviceRegistry.Value.AuditServiceUrl)) + tasks.Add(AddServiceSpec(swaggerDoc, GetDotNetSwaggerSpecUrl(serviceRegistry.Value.AuditServiceUrl))); + + if (!string.IsNullOrEmpty(serviceRegistry.Value.AccountServiceUrl)) + tasks.Add(AddServiceSpec(swaggerDoc, GetDotNetSwaggerSpecUrl(serviceRegistry.Value.AccountServiceUrl))); + + if (!string.IsNullOrEmpty(serviceRegistry.Value.CensusServiceUrl)) + tasks.Add(AddServiceSpec(swaggerDoc, GetJavaSwaggerSpecUrl(serviceRegistry.Value.CensusServiceUrl))); + + if (!string.IsNullOrEmpty(serviceRegistry.Value.DataAcquisitionServiceUrl)) + tasks.Add(AddServiceSpec(swaggerDoc, GetDotNetSwaggerSpecUrl(serviceRegistry.Value.DataAcquisitionServiceUrl))); + + if (!string.IsNullOrEmpty(serviceRegistry.Value.MeasureServiceUrl)) + tasks.Add(AddServiceSpec(swaggerDoc, GetJavaSwaggerSpecUrl(serviceRegistry.Value.MeasureServiceUrl))); + + if (!string.IsNullOrEmpty(serviceRegistry.Value.NormalizationServiceUrl)) + tasks.Add(AddServiceSpec(swaggerDoc, GetDotNetSwaggerSpecUrl(serviceRegistry.Value.NormalizationServiceUrl))); + + if (!string.IsNullOrEmpty(serviceRegistry.Value.NotificationServiceUrl)) + tasks.Add(AddServiceSpec(swaggerDoc, GetDotNetSwaggerSpecUrl(serviceRegistry.Value.NotificationServiceUrl))); + + if (!string.IsNullOrEmpty(serviceRegistry.Value.QueryDispatchServiceUrl)) + tasks.Add(AddServiceSpec(swaggerDoc, GetDotNetSwaggerSpecUrl(serviceRegistry.Value.QueryDispatchServiceUrl))); + + if (!string.IsNullOrEmpty(serviceRegistry.Value.ReportServiceUrl)) + tasks.Add(AddServiceSpec(swaggerDoc, GetDotNetSwaggerSpecUrl(serviceRegistry.Value.ReportServiceUrl))); + + if (!string.IsNullOrEmpty(serviceRegistry.Value.SubmissionServiceUrl)) + tasks.Add(AddServiceSpec(swaggerDoc, GetDotNetSwaggerSpecUrl(serviceRegistry.Value.SubmissionServiceUrl))); + + if (!string.IsNullOrEmpty(serviceRegistry.Value.TenantService?.TenantServiceUrl)) + tasks.Add(AddServiceSpec(swaggerDoc, GetDotNetSwaggerSpecUrl(serviceRegistry.Value.TenantService.TenantServiceUrl))); + + if (!string.IsNullOrEmpty(serviceRegistry.Value.ValidationServiceUrl)) + tasks.Add(AddServiceSpec(swaggerDoc, GetDotNetSwaggerSpecUrl(serviceRegistry.Value.ValidationServiceUrl))); + + Task.WhenAll(tasks).GetAwaiter().GetResult(); + } +} \ No newline at end of file diff --git a/DotNet/LinkAdmin.BFF/LinkAdmin.BFF.csproj b/DotNet/LinkAdmin.BFF/LinkAdmin.BFF.csproj index b8ab44279..c3f8c4238 100644 --- a/DotNet/LinkAdmin.BFF/LinkAdmin.BFF.csproj +++ b/DotNet/LinkAdmin.BFF/LinkAdmin.BFF.csproj @@ -28,6 +28,7 @@ + diff --git a/DotNet/LinkAdmin.BFF/Program.cs b/DotNet/LinkAdmin.BFF/Program.cs index 8801d779e..38b35f19d 100644 --- a/DotNet/LinkAdmin.BFF/Program.cs +++ b/DotNet/LinkAdmin.BFF/Program.cs @@ -13,6 +13,7 @@ using Serilog.Exceptions; using Serilog.Settings.Configuration; using System.Reflection; +using System.Reflection.Metadata; using Microsoft.OpenApi.Models; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.DataProtection; @@ -25,6 +26,7 @@ using LantanaGroup.Link.Shared.Application.Extensions; using LantanaGroup.Link.Shared.Settings; using LantanaGroup.Link.LinkAdmin.BFF.Application.Interfaces.Infrastructure; +using LantanaGroup.Link.LinkAdmin.BFF.Application.Swagger; using LantanaGroup.Link.LinkAdmin.BFF.Infrastructure.Telemetry; using LantanaGroup.Link.Shared.Application.Middleware; using LantanaGroup.Link.Shared.Application.Extensions.ExternalServices; @@ -303,7 +305,7 @@ static void RegisterServices(WebApplicationBuilder builder) var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); c.IncludeXmlComments(xmlPath); - + c.DocumentFilter(); }); // Add logging redaction services diff --git a/DotNet/Shared/Application/Models/Configs/ServiceRegistry.cs b/DotNet/Shared/Application/Models/Configs/ServiceRegistry.cs index b267342fc..c6ce37c74 100644 --- a/DotNet/Shared/Application/Models/Configs/ServiceRegistry.cs +++ b/DotNet/Shared/Application/Models/Configs/ServiceRegistry.cs @@ -16,6 +16,7 @@ public class ServiceRegistry public string QueryDispatchServiceUrl { get; set; } = null!; public string ReportServiceUrl { get; set; } = null!; public string SubmissionServiceUrl { get; set; } = null!; + public string ValidationServiceUrl { get; set; } = null!; public TenantServiceRegistration TenantService { get; set; } = null!; public string AccountServiceApiUrl From b003608a8cdc632812212f632a49cdd4d98edd25 Mon Sep 17 00:00:00 2001 From: Sean McIlvenna Date: Tue, 31 Dec 2024 14:19:02 -0800 Subject: [PATCH 02/11] Re-adding `ServiceSpecAppender` class after merging from `dev` --- DotNet/Admin.BFF/Admin.BFF.csproj | 1 + .../Application/Swagger/ServiceSpecAppender.cs | 0 DotNet/Admin.BFF/Program.cs | 1 + 3 files changed, 2 insertions(+) rename DotNet/{LinkAdmin.BFF => Admin.BFF}/Application/Swagger/ServiceSpecAppender.cs (100%) diff --git a/DotNet/Admin.BFF/Admin.BFF.csproj b/DotNet/Admin.BFF/Admin.BFF.csproj index c3f8c4238..22a04d1e5 100644 --- a/DotNet/Admin.BFF/Admin.BFF.csproj +++ b/DotNet/Admin.BFF/Admin.BFF.csproj @@ -44,6 +44,7 @@ + diff --git a/DotNet/LinkAdmin.BFF/Application/Swagger/ServiceSpecAppender.cs b/DotNet/Admin.BFF/Application/Swagger/ServiceSpecAppender.cs similarity index 100% rename from DotNet/LinkAdmin.BFF/Application/Swagger/ServiceSpecAppender.cs rename to DotNet/Admin.BFF/Application/Swagger/ServiceSpecAppender.cs diff --git a/DotNet/Admin.BFF/Program.cs b/DotNet/Admin.BFF/Program.cs index 93660e0ad..5b404a741 100644 --- a/DotNet/Admin.BFF/Program.cs +++ b/DotNet/Admin.BFF/Program.cs @@ -25,6 +25,7 @@ using LantanaGroup.Link.Shared.Application.Extensions; using LantanaGroup.Link.Shared.Settings; using LantanaGroup.Link.LinkAdmin.BFF.Application.Interfaces.Infrastructure; +using LantanaGroup.Link.LinkAdmin.BFF.Application.Swagger; using LantanaGroup.Link.LinkAdmin.BFF.Infrastructure.Telemetry; using LantanaGroup.Link.Shared.Application.Middleware; using LantanaGroup.Link.Shared.Application.Extensions.ExternalServices; From c7ac5080ae493db025c63b46521eaadd830e3c12 Mon Sep 17 00:00:00 2001 From: Sean McIlvenna Date: Tue, 31 Dec 2024 14:34:56 -0800 Subject: [PATCH 03/11] Moving swagger spec location in Account service to be consistent with other services. Updating BFF service spec appender to correct which services are java vs. DotNet --- DotNet/Account/Program.cs | 11 ++--------- .../Application/Swagger/ServiceSpecAppender.cs | 4 ++-- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/DotNet/Account/Program.cs b/DotNet/Account/Program.cs index 8a29a2556..bfb931700 100644 --- a/DotNet/Account/Program.cs +++ b/DotNet/Account/Program.cs @@ -310,15 +310,8 @@ static void SetupMiddleware(WebApplication app) app.UseExceptionHandler(); } - // Configure swagger - if (app.Configuration.GetValue(ConfigurationConstants.AppSettings.EnableSwagger)) - { - app.UseSwagger(opts => { opts.RouteTemplate = "api/account/swagger/{documentname}/swagger.json"; }); - app.UseSwaggerUI(opts => { - opts.SwaggerEndpoint("/api/account/swagger/v1/swagger.json", $"{ServiceActivitySource.ServiceName} - {ServiceActivitySource.Version}"); - opts.RoutePrefix = "api/account/swagger"; - }); - } + // Configure the HTTP request pipeline. + app.ConfigureSwagger(); app.UseRouting(); app.UseCors(CorsSettings.DefaultCorsPolicyName); diff --git a/DotNet/Admin.BFF/Application/Swagger/ServiceSpecAppender.cs b/DotNet/Admin.BFF/Application/Swagger/ServiceSpecAppender.cs index 587597a93..b74730465 100644 --- a/DotNet/Admin.BFF/Application/Swagger/ServiceSpecAppender.cs +++ b/DotNet/Admin.BFF/Application/Swagger/ServiceSpecAppender.cs @@ -63,7 +63,7 @@ public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) tasks.Add(AddServiceSpec(swaggerDoc, GetDotNetSwaggerSpecUrl(serviceRegistry.Value.AccountServiceUrl))); if (!string.IsNullOrEmpty(serviceRegistry.Value.CensusServiceUrl)) - tasks.Add(AddServiceSpec(swaggerDoc, GetJavaSwaggerSpecUrl(serviceRegistry.Value.CensusServiceUrl))); + tasks.Add(AddServiceSpec(swaggerDoc, GetDotNetSwaggerSpecUrl(serviceRegistry.Value.CensusServiceUrl))); if (!string.IsNullOrEmpty(serviceRegistry.Value.DataAcquisitionServiceUrl)) tasks.Add(AddServiceSpec(swaggerDoc, GetDotNetSwaggerSpecUrl(serviceRegistry.Value.DataAcquisitionServiceUrl))); @@ -90,7 +90,7 @@ public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) tasks.Add(AddServiceSpec(swaggerDoc, GetDotNetSwaggerSpecUrl(serviceRegistry.Value.TenantService.TenantServiceUrl))); if (!string.IsNullOrEmpty(serviceRegistry.Value.ValidationServiceUrl)) - tasks.Add(AddServiceSpec(swaggerDoc, GetDotNetSwaggerSpecUrl(serviceRegistry.Value.ValidationServiceUrl))); + tasks.Add(AddServiceSpec(swaggerDoc, GetJavaSwaggerSpecUrl(serviceRegistry.Value.ValidationServiceUrl))); Task.WhenAll(tasks).GetAwaiter().GetResult(); } From 143e42a15434d0072c5d99a275c6291f60eadba6 Mon Sep 17 00:00:00 2001 From: Sean McIlvenna Date: Tue, 31 Dec 2024 14:37:00 -0800 Subject: [PATCH 04/11] Fixing health check on sql-server docker compose container --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 74f94724f..22fb86813 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -63,7 +63,7 @@ services: - mssql_data:/var/opt/mssql restart: always healthcheck: - test: ["CMD-SHELL", "/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P ${LINK_DB_PASS} -Q 'SELECT 1' || exit 1"] + test: ["CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -C -S localhost -U sa -P ${LINK_DB_PASS} -Q 'SELECT 1' || exit 1"] interval: 10s retries: 10 start_period: 10s From 58c748fe7d7a11973db5fb5230df15bc55d85c59 Mon Sep 17 00:00:00 2001 From: Sean McIlvenna Date: Tue, 31 Dec 2024 15:20:17 -0800 Subject: [PATCH 05/11] Reverting removal of `AddTransient()` calls due to merge --- DotNet/Admin.BFF/Program.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/DotNet/Admin.BFF/Program.cs b/DotNet/Admin.BFF/Program.cs index 5b404a741..639deffe2 100644 --- a/DotNet/Admin.BFF/Program.cs +++ b/DotNet/Admin.BFF/Program.cs @@ -115,11 +115,14 @@ static void RegisterServices(WebApplicationBuilder builder) // Add commands builder.Services.AddTransient(); + builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); //Add Redis if (builder.Configuration.GetValue("Cache:Enabled")) From 28e222b7b4461c8b4295e5dda95ef463ca3dc184 Mon Sep 17 00:00:00 2001 From: Sean McIlvenna Date: Fri, 3 Jan 2025 08:29:36 -0800 Subject: [PATCH 06/11] Enhancements to ServiceSpecAppender to have a more flexible configuration --- .../ServiceSpecAppenderConfig.cs | 54 +++++ .../Swagger/ServiceSpecAppender.cs | 184 ++++++++++++++---- DotNet/Admin.BFF/Program.cs | 4 +- DotNet/Admin.BFF/appsettings.json | 38 ++++ 4 files changed, 244 insertions(+), 36 deletions(-) create mode 100644 DotNet/Admin.BFF/Application/Models/Configuration/ServiceSpecAppenderConfig.cs diff --git a/DotNet/Admin.BFF/Application/Models/Configuration/ServiceSpecAppenderConfig.cs b/DotNet/Admin.BFF/Application/Models/Configuration/ServiceSpecAppenderConfig.cs new file mode 100644 index 000000000..bfe5cccf7 --- /dev/null +++ b/DotNet/Admin.BFF/Application/Models/Configuration/ServiceSpecAppenderConfig.cs @@ -0,0 +1,54 @@ +namespace LantanaGroup.Link.LinkAdmin.BFF.Application.Models.Configuration; + +public class ServiceSpecAppenderConfig +{ + public static string ConfigSectionName = "ServiceSpecAppender"; + + public string AccountServiceSpecUrl { get; set; } = null!; + public string AccountServiceBffPrefix { get; set; } = null!; + public string AccountServiceActualPrefix { get; set; } = null!; + + public string AuditServiceSpecUrl { get; set; } = null!; + public string AuditServiceBffPrefix { get; set; } = null!; + public string AuditServiceActualPrefix { get; set; } = null!; + + public string CensusServiceSpecUrl { get; set; } = null!; + public string CensusServiceBffPrefix { get; set; } = null!; + public string CensusServiceActualPrefix { get; set; } = null!; + + public string DataAcquisitionServiceSpecUrl { get; set; } = null!; + public string DataAcquisitionServiceBffPrefix { get; set; } = null!; + public string DataAcquisitionServiceActualPrefix { get; set; } = null!; + + public string MeasureServiceSpecUrl { get; set; } = null!; + public string MeasureServiceBffPrefix { get; set; } = null!; + public string MeasureServiceActualPrefix { get; set; } = null!; + + public string NormalizationServiceSpecUrl { get; set; } = null!; + public string NormalizationServiceBffPrefix { get; set; } = null!; + public string NormalizationServiceActualPrefix { get; set; } = null!; + + public string NotificationServiceSpecUrl { get; set; } = null!; + public string NotificationServiceBffPrefix { get; set; } = null!; + public string NotificationServiceActualPrefix { get; set; } = null!; + + public string QueryDispatchServiceSpecUrl { get; set; } = null!; + public string QueryDispatchServiceBffPrefix { get; set; } = null!; + public string QueryDispatchServiceActualPrefix { get; set; } = null!; + + public string ReportServiceSpecUrl { get; set; } = null!; + public string ReportServiceBffPrefix { get; set; } = null!; + public string ReportServiceActualPrefix { get; set; } = null!; + + public string SubmissionServiceSpecUrl { get; set; } = null!; + public string SubmissionServiceBffPrefix { get; set; } = null!; + public string SubmissionServiceActualPrefix { get; set; } = null!; + + public string TenantServiceSpecUrl { get; set; } = null!; + public string TenantServiceBffPrefix { get; set; } = null!; + public string TenantServiceActualPrefix { get; set; } = null!; + + public string ValidationServiceSpecUrl { get; set; } = null!; + public string ValidationServiceBffPrefix { get; set; } = null!; + public string ValidationServiceActualPrefix { get; set; } = null!; +} \ No newline at end of file diff --git a/DotNet/Admin.BFF/Application/Swagger/ServiceSpecAppender.cs b/DotNet/Admin.BFF/Application/Swagger/ServiceSpecAppender.cs index b74730465..b9c5f9eb4 100644 --- a/DotNet/Admin.BFF/Application/Swagger/ServiceSpecAppender.cs +++ b/DotNet/Admin.BFF/Application/Swagger/ServiceSpecAppender.cs @@ -1,7 +1,9 @@ -using LantanaGroup.Link.Shared.Application.Models.Configs; +using LantanaGroup.Link.LinkAdmin.BFF.Application.Models.Configuration; +using LantanaGroup.Link.Shared.Application.Models.Configs; using Microsoft.Extensions.Options; using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Readers; +using SharpYaml.Tokens; using Swashbuckle.AspNetCore.SwaggerGen; namespace LantanaGroup.Link.LinkAdmin.BFF.Application.Swagger; @@ -9,7 +11,8 @@ namespace LantanaGroup.Link.LinkAdmin.BFF.Application.Swagger; public class ServiceSpecAppender( IOptions serviceRegistry, HttpClient httpClient, - ILogger logger) : IDocumentFilter + ILogger logger, + IOptions config) : IDocumentFilter { private static readonly Dictionary _responseCache = new(); @@ -35,62 +38,175 @@ private static string GetJavaSwaggerSpecUrl(string serviceUrl) return serviceUrl.TrimEnd('/') + "/v3/api-docs"; } - private async Task AddServiceSpec(OpenApiDocument swaggerDoc, string swaggerSpecUrl) + private async Task AddServiceSpec(OpenApiDocument doc, string? serviceUrl, string? specUrl, string bffPrefix, string actualPrefix) { + if (serviceUrl == null || specUrl == null) return; + + var fullSpecUrl = serviceUrl.TrimEnd('/') + "/" + specUrl.TrimStart('/'); + try { - var serviceSpec = await GetServiceSpec(swaggerSpecUrl); - foreach (var path in serviceSpec.Paths) + logger.LogInformation("Adding service spec {fullSpecUrl} to swagger doc", fullSpecUrl); + var spec = await GetServiceSpec(fullSpecUrl); + + foreach (var schema in spec.Components.Schemas) + { + var serviceSpecPrefix = spec.Info?.Title?.Replace(" ", string.Empty) ?? + specUrl.GetHashCode().ToString(); + var schemaName = schema.Key; + var newSchemaName = schemaName; + + // Check for duplicates and rename if necessary + if (doc.Components.Schemas.ContainsKey(schemaName)) + { + newSchemaName = $"{serviceSpecPrefix}{schemaName}"; + RenameSchemaReferences(spec, schemaName, newSchemaName); + } + + doc.Components.Schemas[newSchemaName] = schema.Value; + } + + foreach (var path in spec.Paths) { + PrefixOperationTags(path.Value, spec); + + string newPath = !string.IsNullOrEmpty(actualPrefix) + ? path.Key.Replace(actualPrefix, bffPrefix) + : path.Key; + + logger.LogDebug($"Adding path {newPath} (originally {path.Key} from {serviceUrl}"); + // Assumes that the proxy path of the service is the same as the paths in the service spec - swaggerDoc.Paths.Add(path.Key, path.Value); + doc.Paths.Add(newPath, path.Value); } } catch (Exception ex) { - logger.LogWarning(ex, "Failed to add service spec to swagger doc"); + logger.LogWarning($"Failed to add service spec {fullSpecUrl} to swagger doc: {ex.Message}"); } } - public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) + private static void PrefixOperationTags(OpenApiPathItem path, OpenApiDocument spec) { - var tasks = new List(); - - if (!string.IsNullOrEmpty(serviceRegistry.Value.AuditServiceUrl)) - tasks.Add(AddServiceSpec(swaggerDoc, GetDotNetSwaggerSpecUrl(serviceRegistry.Value.AuditServiceUrl))); + foreach (var operation in path.Operations) + { + if (operation.Value.Tags.Count == 0) + { + if (spec.Info?.Title != null) + operation.Value.Tags.Add(new OpenApiTag() { Name = spec.Info?.Title }); + } + else + { + // Ensure the service spec's title is prefixed on each of the tags + foreach (var tag in operation.Value.Tags) + { + if (spec.Info?.Title != tag.Name) + tag.Name = $"{spec.Info?.Title} - {tag.Name}"; + } + } + } + } - if (!string.IsNullOrEmpty(serviceRegistry.Value.AccountServiceUrl)) - tasks.Add(AddServiceSpec(swaggerDoc, GetDotNetSwaggerSpecUrl(serviceRegistry.Value.AccountServiceUrl))); + private void RenameSchemaReferences(OpenApiDocument serviceSpec, string oldName, string newName) + { + var processedSchemas = new HashSet(); + + foreach (var path in serviceSpec.Paths.Values) + { + foreach (var operation in path.Operations.Values) + { + RenameSchemaInParameters(operation.Parameters, oldName, newName); + RenameSchemaInRequestBody(operation.RequestBody, oldName, newName); + RenameSchemaInResponses(operation.Responses, oldName, newName); + } + } - if (!string.IsNullOrEmpty(serviceRegistry.Value.CensusServiceUrl)) - tasks.Add(AddServiceSpec(swaggerDoc, GetDotNetSwaggerSpecUrl(serviceRegistry.Value.CensusServiceUrl))); + foreach (var component in serviceSpec.Components.Schemas.Values) + { + RenameSchemaInSchema(component, oldName, newName, processedSchemas); + } + } - if (!string.IsNullOrEmpty(serviceRegistry.Value.DataAcquisitionServiceUrl)) - tasks.Add(AddServiceSpec(swaggerDoc, GetDotNetSwaggerSpecUrl(serviceRegistry.Value.DataAcquisitionServiceUrl))); + private void RenameSchemaInParameters(IList parameters, string oldName, string newName) + { + foreach (var parameter in parameters) + { + if (parameter.Schema.Reference != null && parameter.Schema.Reference.Id == oldName) + { + parameter.Schema.Reference.Id = newName; + } + } + } - if (!string.IsNullOrEmpty(serviceRegistry.Value.MeasureServiceUrl)) - tasks.Add(AddServiceSpec(swaggerDoc, GetJavaSwaggerSpecUrl(serviceRegistry.Value.MeasureServiceUrl))); + private void RenameSchemaInRequestBody(OpenApiRequestBody requestBody, string oldName, string newName) + { + if (requestBody == null) return; - if (!string.IsNullOrEmpty(serviceRegistry.Value.NormalizationServiceUrl)) - tasks.Add(AddServiceSpec(swaggerDoc, GetDotNetSwaggerSpecUrl(serviceRegistry.Value.NormalizationServiceUrl))); + foreach (var content in requestBody.Content.Values) + { + if (content?.Schema?.Reference != null && content?.Schema?.Reference?.Id == oldName) + { + content.Schema.Reference.Id = newName; + } + } + } - if (!string.IsNullOrEmpty(serviceRegistry.Value.NotificationServiceUrl)) - tasks.Add(AddServiceSpec(swaggerDoc, GetDotNetSwaggerSpecUrl(serviceRegistry.Value.NotificationServiceUrl))); + private void RenameSchemaInResponses(OpenApiResponses responses, string oldName, string newName) + { + foreach (var response in responses.Values) + { + foreach (var content in response.Content.Values) + { + if (content.Schema.Reference != null && content.Schema.Reference.Id == oldName) + { + content.Schema.Reference.Id = newName; + } + } + } + } - if (!string.IsNullOrEmpty(serviceRegistry.Value.QueryDispatchServiceUrl)) - tasks.Add(AddServiceSpec(swaggerDoc, GetDotNetSwaggerSpecUrl(serviceRegistry.Value.QueryDispatchServiceUrl))); + private void RenameSchemaInSchema(OpenApiSchema schema, string oldName, string newName, HashSet processedSchemas) + { + if (processedSchemas.Contains(schema)) + { + return; + } - if (!string.IsNullOrEmpty(serviceRegistry.Value.ReportServiceUrl)) - tasks.Add(AddServiceSpec(swaggerDoc, GetDotNetSwaggerSpecUrl(serviceRegistry.Value.ReportServiceUrl))); + processedSchemas.Add(schema); - if (!string.IsNullOrEmpty(serviceRegistry.Value.SubmissionServiceUrl)) - tasks.Add(AddServiceSpec(swaggerDoc, GetDotNetSwaggerSpecUrl(serviceRegistry.Value.SubmissionServiceUrl))); + if (schema.Reference != null && schema.Reference.Id == oldName) + { + schema.Reference.Id = newName; + } - if (!string.IsNullOrEmpty(serviceRegistry.Value.TenantService?.TenantServiceUrl)) - tasks.Add(AddServiceSpec(swaggerDoc, GetDotNetSwaggerSpecUrl(serviceRegistry.Value.TenantService.TenantServiceUrl))); + foreach (var property in schema.Properties.Values) + { + RenameSchemaInSchema(property, oldName, newName, processedSchemas); + } + } + + public static string GetFullSpecUrl(string serviceUrl, string specUrl) + { + return serviceUrl.TrimEnd('/') + specUrl; + } - if (!string.IsNullOrEmpty(serviceRegistry.Value.ValidationServiceUrl)) - tasks.Add(AddServiceSpec(swaggerDoc, GetJavaSwaggerSpecUrl(serviceRegistry.Value.ValidationServiceUrl))); + public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) + { + var tasks = new List + { + AddServiceSpec(swaggerDoc, serviceRegistry.Value.AccountServiceUrl, config.Value.AccountServiceSpecUrl, config.Value.AccountServiceBffPrefix, config.Value.AccountServiceActualPrefix), + AddServiceSpec(swaggerDoc, serviceRegistry.Value.AuditServiceUrl, config.Value.AuditServiceSpecUrl, config.Value.AuditServiceBffPrefix, config.Value.AuditServiceActualPrefix), + AddServiceSpec(swaggerDoc, serviceRegistry.Value.CensusServiceUrl, config.Value.CensusServiceSpecUrl, config.Value.CensusServiceBffPrefix, config.Value.CensusServiceActualPrefix), + AddServiceSpec(swaggerDoc, serviceRegistry.Value.DataAcquisitionServiceUrl, config.Value.DataAcquisitionServiceSpecUrl, config.Value.DataAcquisitionServiceBffPrefix, config.Value.DataAcquisitionServiceActualPrefix), + AddServiceSpec(swaggerDoc, serviceRegistry.Value.MeasureServiceUrl, config.Value.MeasureServiceSpecUrl, config.Value.MeasureServiceBffPrefix, config.Value.MeasureServiceActualPrefix), + AddServiceSpec(swaggerDoc, serviceRegistry.Value.NormalizationServiceUrl, config.Value.NormalizationServiceSpecUrl, config.Value.NormalizationServiceBffPrefix, config.Value.NormalizationServiceActualPrefix), + AddServiceSpec(swaggerDoc, serviceRegistry.Value.NotificationServiceUrl, config.Value.NotificationServiceSpecUrl, config.Value.NotificationServiceBffPrefix, config.Value.NotificationServiceActualPrefix), + AddServiceSpec(swaggerDoc, serviceRegistry.Value.QueryDispatchServiceUrl, config.Value.QueryDispatchServiceSpecUrl, config.Value.QueryDispatchServiceBffPrefix, config.Value.QueryDispatchServiceActualPrefix), + AddServiceSpec(swaggerDoc, serviceRegistry.Value.ReportServiceUrl, config.Value.ReportServiceSpecUrl, config.Value.ReportServiceBffPrefix, config.Value.ReportServiceActualPrefix), + AddServiceSpec(swaggerDoc, serviceRegistry.Value.SubmissionServiceUrl, config.Value.SubmissionServiceSpecUrl, config.Value.SubmissionServiceBffPrefix, config.Value.SubmissionServiceActualPrefix), + AddServiceSpec(swaggerDoc, serviceRegistry.Value.TenantService.TenantServiceUrl, config.Value.TenantServiceSpecUrl, config.Value.TenantServiceBffPrefix, config.Value.TenantServiceActualPrefix), + AddServiceSpec(swaggerDoc, serviceRegistry.Value.ValidationServiceUrl, config.Value.ValidationServiceSpecUrl, config.Value.ValidationServiceBffPrefix, config.Value.ValidationServiceActualPrefix) + }; Task.WhenAll(tasks).GetAwaiter().GetResult(); } diff --git a/DotNet/Admin.BFF/Program.cs b/DotNet/Admin.BFF/Program.cs index 639deffe2..8652b12c9 100644 --- a/DotNet/Admin.BFF/Program.cs +++ b/DotNet/Admin.BFF/Program.cs @@ -90,6 +90,7 @@ static void RegisterServices(WebApplicationBuilder builder) builder.Services.Configure(builder.Configuration.GetSection(ServiceRegistry.ConfigSectionName)); builder.Services.Configure(builder.Configuration.GetSection(ConfigurationConstants.AppSettings.LinkTokenService)); builder.Services.Configure(builder.Configuration.GetSection(ConfigurationConstants.AppSettings.Cache)); + builder.Services.Configure(builder.Configuration.GetSection(ServiceSpecAppenderConfig.ConfigSectionName)); // Determine if anonymous access is allowed var allowAnonymousAccess = builder.Configuration.GetValue("Authentication:EnableAnonymousAccess"); @@ -223,8 +224,7 @@ static void RegisterServices(WebApplicationBuilder builder) { healthCheckBuilder.AddCheck("Cache"); } - - + // Add swagger generation builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(c => diff --git a/DotNet/Admin.BFF/appsettings.json b/DotNet/Admin.BFF/appsettings.json index 65f6cb097..2555ebb41 100644 --- a/DotNet/Admin.BFF/appsettings.json +++ b/DotNet/Admin.BFF/appsettings.json @@ -97,6 +97,44 @@ "OtelCollectorEndpoint": "", "EnableAzureMonitor": false }, + "ServiceSpecAppender": { + "AccountServiceSpecUrl": "/swagger/v1/swagger.json", + "AccountServiceBffPrefix": "/api/account", + "AccountServiceActualPrefix": "/api", + "AuditServiceSpecUrl": "/swagger/v1/swagger.json", + "AuditServiceBffPrefix": "/api/audit", + "AuditServiceActualPrefix": "/api", + "CensusServiceSpecUrl": "/swagger/v1/swagger.json", + "CensusServiceBffPrefix": "/api/census", + "CensusServiceActualPrefix": "/api/census", + "DataAcquisitionServiceSpecUrl": "/swagger/v1/swagger.json", + "DataAcquisitionServiceBffPrefix": "/api/data", + "DataAcquisitionServiceActualPrefix": "/api", + "MeasureServiceSpecUrl": "/v3/api-docs", + "MeasureServiceBffPrefix": "/api/measure", + "MeasureServiceActualPrefix": "/api", + "NormalizationServiceSpecUrl": "/swagger/v1/swagger.json", + "NormalizationServiceBffPrefix": "/api/normalization", + "NormalizationServiceActualPrefix": "/api", + "NotificationServiceSpecUrl": "/swagger/v1/swagger.json", + "NotificationServiceBffPrefix": "/api/notification", + "NotificationServiceActualPrefix": "/api", + "QueryDispatchServiceSpecUrl": "/swagger/v1/swagger.json", + "QueryDispatchServiceBffPrefix": "/api/querydispatch", + "QueryDispatchServiceActualPrefix": "/api", + "ReportServiceSpecUrl": "/swagger/v1/swagger.json", + "ReportServiceBffPrefix": "/api/report", + "ReportServiceActualPrefix": "/api", + "SubmissionServiceSpecUrl": "/swagger/v1/swagger.json", + "SubmissionServiceBffPrefix": "/api/submission", + "SubmissionServiceActualPrefix": "/api", + "TenantServiceSpecUrl": "/swagger/v1/swagger.json", + "TenantServiceBffPrefix": "/api/facility", + "TenantServiceActualPrefix": "/api", + "ValidationServiceSpecUrl": "/v3/api-docs", + "ValidationServiceBffPrefix": "/api/validation", + "ValidationServiceActualPrefix": "/api" + }, "Logging": { "LogLevel": { "Default": "Information", From 46db1e54f178426d8e876dcdc0ae24a0ba9741f7 Mon Sep 17 00:00:00 2001 From: Sean McIlvenna Date: Fri, 3 Jan 2025 08:31:02 -0800 Subject: [PATCH 07/11] Updating docs: * Moving details about submission folder to the functionality section * Providing information about how YARP configuration works in service spec --- docs/functionality/submission_folder.md | 13 +++++++++ docs/service_specs/admin_ui.md | 10 +++---- docs/service_specs/bff.md | 39 +++++++++++++++++++++++++ docs/service_specs/census.md | 2 ++ docs/service_specs/measure_eval.md | 2 ++ docs/service_specs/normalization.md | 2 ++ docs/service_specs/notification.md | 2 ++ docs/service_specs/submission.md | 15 ++-------- docs/service_specs/tenant.md | 2 ++ 9 files changed, 69 insertions(+), 18 deletions(-) diff --git a/docs/functionality/submission_folder.md b/docs/functionality/submission_folder.md index f5e296d20..5955a818e 100644 --- a/docs/functionality/submission_folder.md +++ b/docs/functionality/submission_folder.md @@ -10,6 +10,19 @@ Example: `1234-NHSNdQMAcuteCareHospitalInitialPopulation+NHSNGlycemicControlHypo ### Files +| File | Description | Multiple Files? | +| ---- | ---- | ---- | +| Aggregate | A [MeasureReport](https://hl7.org/fhir/R4/measurereport.html) resource that contains references to each patient evaluation for a specific measure | Yes, one per measure | +| Patient List | A [List](https://hl7.org/fhir/R4/list.html) resource of all patients that were admitted into the facility during the reporting period | No | +| Device | A [Device](https://hl7.org/fhir/R4/device.html) resource that details the version of Link Cloud that was used | No | +| Organization | An [Organization](https://hl7.org/fhir/R4/organization.html) resource for the submitting facility | No | +| Other Resources | A [Bundle](https://hl7.org/fhir/R4/bundle.html) resource that contains all of the shared resources (Location, Medication, etc) that are referenced in the patient Measure Reports | No | +| Patient | A [Bundle](https://hl7.org/fhir/R4/bundle.html) resource that contains the MeasureReports and related resources for a patient | Yes, one per evaluated patient | + +An example of the submission package can be found at `\link-cloud\Submission Example`. + +Example File Names: + - `_manifest.json` - `submitting-device.json` - The submitting Device (always “NHSNLink” for reports generated by NHSNLink) - `submitting-org.json` - The submitting Organization diff --git a/docs/service_specs/admin_ui.md b/docs/service_specs/admin_ui.md index 95c4a570b..ef1e23106 100644 --- a/docs/service_specs/admin_ui.md +++ b/docs/service_specs/admin_ui.md @@ -2,18 +2,16 @@ ## Admin UI -> ⚠️ **Note:** This service is currently called "demo app" and is planned to be renamed. - -See [Admin UI Functionality](../functionality/admin_ui.md) for more information on the role of the Admin UI service in the Link Cloud ecosystem. - -## Admin UI Overview - - **Technology**: JavaScript (TypeScript) & Angular - **Image Name**: link-admin-ui - **Port**: 80 - **Database**: NONE - **Scale**: 0-5 +## Related Documentation + +See [Admin UI Functionality](../functionality/admin_ui.md) for more information on the role of the Admin UI service in the Link Cloud ecosystem. + ## Volumes | Volume | Mount Path | Sub-path | diff --git a/docs/service_specs/bff.md b/docs/service_specs/bff.md index 126d4c731..a0c6e75b8 100644 --- a/docs/service_specs/bff.md +++ b/docs/service_specs/bff.md @@ -10,8 +10,12 @@ - **Database**: NONE - **Scale**: 0-3 +## Related Documentation + See [Admin UI Functionality](../functionality/admin_ui.md) for more information on the role of the BFF service in the Link Cloud ecosystem. +See [BFF](bff.md) for more information on the BFF pattern that is used to support this Admin UI. + ## Common Configurations * [Swagger](../config/csharp.md#swagger) @@ -33,6 +37,41 @@ See [Admin UI Functionality](../functionality/admin_ui.md) for more information | Redis__Password | \ | Redis password | Yes | | ConnectionStrings__Redis | `` | Connection string for Redis | Yes | +## Gateway/Routing + +The service is configured via `appsettings.json` to proxy (act as a gateway) for all the underlying micro services, so that the endpoints of the underlying micro services can be exposed to the user interface. This is the reason security _must_ be enabled for all micro services when deployed to a non-development environment. + +An example of the YARP configuration is as follows: + +```json +{ + "ReverseProxy": { + "Routes": { + "route1": { + "ClusterId": "AccountService", + "AuthorizationPolicy": "AuthenticatedUser", + "Match": { + "Path": "api/account/{**catch-all}" + } + } + }, + "Clusters": { + "AccountService": { + "Destinations": { + "destination1": { + "Address": "" + } + } + } + } + } +} +``` + +The `Address` property above is left blank as a hint that the actual address should be set at runtime via the deployment's configuration, such as an environment variable `ReverseProxy__Clusters__AccountService__Destinations__destination1_Address` set to the `https://XXX` address that the account service is deployed to. + +In the above configuration, if the `AccountService` is deployed to `https://account-service`, and the BFF is deployed to `https://bff.mycompany.com`, then requests to `https://bff.mycompany.com/api/account/**` will be proxied to `https://account-service/**`. + ## API Operations The **BFF** service provides REST endpoints to support user authentication, session management, and integration testing. These endpoints serve as a bridge between the frontend and backend systems. diff --git a/docs/service_specs/census.md b/docs/service_specs/census.md index 7f3b47307..a3a7d08a8 100644 --- a/docs/service_specs/census.md +++ b/docs/service_specs/census.md @@ -10,6 +10,8 @@ The Census service is primarily responsible for maintaining a tenants admit and - **Database**: MSSQL (previously Mongo) - **Scale**: 0-3 +## Related Documentation + See [Census Functionality](../functionality/census_management.md) for more information on the role of the Census service in the Link Cloud ecosystem. ## Common Configurations diff --git a/docs/service_specs/measure_eval.md b/docs/service_specs/measure_eval.md index 6335004f2..fc4de18bd 100644 --- a/docs/service_specs/measure_eval.md +++ b/docs/service_specs/measure_eval.md @@ -9,6 +9,8 @@ The Measure Eval service is a Java based application that is primarily responsib - **Port**: 8067 - **Database**: Mongo +## Related Documentation + See [Measure Eval Functionality](../functionality/measure_eval.md) for more information on the role of the Measure Eval service in the Link Cloud ecosystem. ## Common Configurations diff --git a/docs/service_specs/normalization.md b/docs/service_specs/normalization.md index 55da09f4e..50e32c896 100644 --- a/docs/service_specs/normalization.md +++ b/docs/service_specs/normalization.md @@ -10,6 +10,8 @@ FHIR resources queried from EHR endpoints can vary from location to location. Th - **Database**: MSSQL - **Scale**: 0-3 +## Related Documentation + See [Normalization Functionality](../functionality/normalization.md) for more information on the role of the Normalization service in the Link Cloud ecosystem. ## Common Configurations diff --git a/docs/service_specs/notification.md b/docs/service_specs/notification.md index 712f6b0f7..ce76d4d93 100644 --- a/docs/service_specs/notification.md +++ b/docs/service_specs/notification.md @@ -10,6 +10,8 @@ The Notification service is responsible for emailing configured users when a not - **Database**: MSSQL - **Scale**: 0-3 +## Related Documentation + See [Notification Functionality](../functionality/notifications.md) for more information on the role of the Notification service in the Link Cloud ecosystem. ## Common Configurations diff --git a/docs/service_specs/submission.md b/docs/service_specs/submission.md index 8bda087cb..312160147 100644 --- a/docs/service_specs/submission.md +++ b/docs/service_specs/submission.md @@ -2,18 +2,7 @@ ## Submission Overview -The Submission service is responsible for packaging a tenant's reporting content and submitting them to a configured destination. Currently, the service only writes the submission content to its local file store. The submission package for a reporting period includes the following files: - -| File | Description | Multiple Files? | -| ---- | ---- | ---- | -| Aggregate | A [MeasureReport](https://hl7.org/fhir/R4/measurereport.html) resource that contains references to each patient evaluation for a specific measure | Yes, one per measure | -| Patient List | A [List](https://hl7.org/fhir/R4/list.html) resource of all patients that were admitted into the facility during the reporting period | No | -| Device | A [Device](https://hl7.org/fhir/R4/device.html) resource that details the version of Link Cloud that was used | No | -| Organization | An [Organization](https://hl7.org/fhir/R4/organization.html) resource for the submitting facility | No | -| Other Resources | A [Bundle](https://hl7.org/fhir/R4/bundle.html) resource that contains all of the shared resources (Location, Medication, etc) that are referenced in the patient Measure Reports | No | -| Patient | A [Bundle](https://hl7.org/fhir/R4/bundle.html) resource that contains the MeasureReports and related resources for a patient | Yes, one per evaluated patient | - -An example of the submission package can be found at `\link-cloud\Submission Example`. +The Submission service is responsible for packaging a tenant's reporting content and submitting them to a configured destination. Currently, the service only writes the submission content to its local file store. - **Technology**: .NET Core - **Image Name**: link-submission @@ -21,6 +10,8 @@ An example of the submission package can be found at `\link-cloud\Submission Exa - **Database**: MongoDB - **Volumes**: Azure Storage Account File Share mounted at `/Link/Submission` +## Related Documentation + See [Submission Functionality](../functionality/submission_folder.md) for more information on the role of the Submission service in the Link Cloud ecosystem. ## Common Configurations diff --git a/docs/service_specs/tenant.md b/docs/service_specs/tenant.md index f79a20b22..92fdc1661 100644 --- a/docs/service_specs/tenant.md +++ b/docs/service_specs/tenant.md @@ -10,6 +10,8 @@ The Tenant service is the entry point for configuring a tenant into Link Cloud. - **Database**: MSSQL - **Scale**: 0-3 +## Related Documentation + See [Tenant Functionality](../functionality/tenant_mgmt.md) for more information on the role of the Tenant service in the Link Cloud ecosystem. ## Common Configurations From f40865375a7dfbfa9556b65461f6ec065af246fe Mon Sep 17 00:00:00 2001 From: Sean McIlvenna Date: Fri, 3 Jan 2025 08:40:00 -0800 Subject: [PATCH 08/11] Using concurrent dictionary for thread safety. Updating Microsoft.OpenApi.Readers package to newer version. --- DotNet/Admin.BFF/Admin.BFF.csproj | 2 +- .../Application/Swagger/ServiceSpecAppender.cs | 15 +++------------ 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/DotNet/Admin.BFF/Admin.BFF.csproj b/DotNet/Admin.BFF/Admin.BFF.csproj index 22a04d1e5..9f2f1915f 100644 --- a/DotNet/Admin.BFF/Admin.BFF.csproj +++ b/DotNet/Admin.BFF/Admin.BFF.csproj @@ -28,7 +28,7 @@ - + diff --git a/DotNet/Admin.BFF/Application/Swagger/ServiceSpecAppender.cs b/DotNet/Admin.BFF/Application/Swagger/ServiceSpecAppender.cs index b9c5f9eb4..c48df54e2 100644 --- a/DotNet/Admin.BFF/Application/Swagger/ServiceSpecAppender.cs +++ b/DotNet/Admin.BFF/Application/Swagger/ServiceSpecAppender.cs @@ -1,4 +1,5 @@ -using LantanaGroup.Link.LinkAdmin.BFF.Application.Models.Configuration; +using System.Collections.Concurrent; +using LantanaGroup.Link.LinkAdmin.BFF.Application.Models.Configuration; using LantanaGroup.Link.Shared.Application.Models.Configs; using Microsoft.Extensions.Options; using Microsoft.OpenApi.Models; @@ -14,7 +15,7 @@ public class ServiceSpecAppender( ILogger logger, IOptions config) : IDocumentFilter { - private static readonly Dictionary _responseCache = new(); + private static readonly ConcurrentDictionary _responseCache = new(); private async Task GetServiceSpec(string swaggerSpecUrl) { @@ -28,16 +29,6 @@ private async Task GetServiceSpec(string swaggerSpecUrl) return openApiDocument; } - private static string GetDotNetSwaggerSpecUrl(string serviceUrl) - { - return serviceUrl.TrimEnd('/') + "/swagger/v1/swagger.json"; - } - - private static string GetJavaSwaggerSpecUrl(string serviceUrl) - { - return serviceUrl.TrimEnd('/') + "/v3/api-docs"; - } - private async Task AddServiceSpec(OpenApiDocument doc, string? serviceUrl, string? specUrl, string bffPrefix, string actualPrefix) { if (serviceUrl == null || specUrl == null) return; From 4098e29baaf8963b96078acc63c248c9b32467b3 Mon Sep 17 00:00:00 2001 From: Sean McIlvenna Date: Fri, 3 Jan 2025 09:03:10 -0800 Subject: [PATCH 09/11] Add documentation (including diagram) for how ServiceSpecAppender works --- docs/service_specs/bff.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/docs/service_specs/bff.md b/docs/service_specs/bff.md index a0c6e75b8..fa291fdf1 100644 --- a/docs/service_specs/bff.md +++ b/docs/service_specs/bff.md @@ -72,6 +72,33 @@ The `Address` property above is left blank as a hint that the actual address sho In the above configuration, if the `AccountService` is deployed to `https://account-service`, and the BFF is deployed to `https://bff.mycompany.com`, then requests to `https://bff.mycompany.com/api/account/**` will be proxied to `https://account-service/**`. +## Swagger Spec Generation + +The swagger spec that is generated for the BFF service is a combination of the BFF service's own endpoints and the endpoints of the underlying micro services that the BFF service proxies for. The swagger spec is generated at runtime by the BFF service, and is available at the `/swagger/v1/swagger.json` endpoint. + +```mermaid +sequenceDiagram + participant BFF as BFF Swagger Generator + participant ServiceRegistry as Service Registry + participant ServiceA as Service A + + loop Fetch and cache for each service + BFF->>ServiceRegistry: Retrieve service URL + + alt Spec is cached + BFF->>BFF: Return spec from cache + else Spec is not cached + BFF->>ServiceA: Fetch Specification + BFF->>BFF: Cache Spec + ServiceA-->>BFF: Return Spec + end + + BFF->>BFF: Merge Service Spec Schemas + BFF->>BFF: Add Prefix to Operation Tags + BFF->>BFF: Merge Service Spec Operations + end +``` + ## API Operations The **BFF** service provides REST endpoints to support user authentication, session management, and integration testing. These endpoints serve as a bridge between the frontend and backend systems. From f84f47c9be7a488c4ac380bc80015ffaa204d454 Mon Sep 17 00:00:00 2001 From: Sean McIlvenna Date: Fri, 3 Jan 2025 09:30:03 -0800 Subject: [PATCH 10/11] Reverting package increment for Microsoft.OpenApi.Readers. When incremented, the swagger spec returns a newer `version` value that is not supported by the currently configured swagger UI. --- DotNet/Admin.BFF/Admin.BFF.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DotNet/Admin.BFF/Admin.BFF.csproj b/DotNet/Admin.BFF/Admin.BFF.csproj index 9f2f1915f..22a04d1e5 100644 --- a/DotNet/Admin.BFF/Admin.BFF.csproj +++ b/DotNet/Admin.BFF/Admin.BFF.csproj @@ -28,7 +28,7 @@ - + From 65ff976c52188f7bd38752a6dfd200676308f996 Mon Sep 17 00:00:00 2001 From: Sean McIlvenna Date: Mon, 6 Jan 2025 13:03:10 -0800 Subject: [PATCH 11/11] Adding a note about the account service's "minimal endpoint" pattern to docs. Improvements to ServiceSpecAppender in what work it does with multiple threads. Adding validation service to the YarpConfigFilter --- .../Swagger/ServiceSpecAppender.cs | 79 ++++++++++++++----- .../Filters/YarpConfigFilter.cs | 2 +- docs/service_specs/account.md | 4 + 3 files changed, 63 insertions(+), 22 deletions(-) diff --git a/DotNet/Admin.BFF/Application/Swagger/ServiceSpecAppender.cs b/DotNet/Admin.BFF/Application/Swagger/ServiceSpecAppender.cs index c48df54e2..a799675c0 100644 --- a/DotNet/Admin.BFF/Application/Swagger/ServiceSpecAppender.cs +++ b/DotNet/Admin.BFF/Application/Swagger/ServiceSpecAppender.cs @@ -29,16 +29,31 @@ private async Task GetServiceSpec(string swaggerSpecUrl) return openApiDocument; } - private async Task AddServiceSpec(OpenApiDocument doc, string? serviceUrl, string? specUrl, string bffPrefix, string actualPrefix) + private async Task<(OpenApiDocument? spec, string specUrl, string bffPrefix, string actualPrefix)> GetServiceSpec(string? serviceUrl, string? specUrl, string bffPrefix, + string actualPrefix) { - if (serviceUrl == null || specUrl == null) return; + if (serviceUrl == null || specUrl == null) return (null, string.Empty, string.Empty, string.Empty); var fullSpecUrl = serviceUrl.TrimEnd('/') + "/" + specUrl.TrimStart('/'); - + try { logger.LogInformation("Adding service spec {fullSpecUrl} to swagger doc", fullSpecUrl); var spec = await GetServiceSpec(fullSpecUrl); + return (spec, fullSpecUrl, bffPrefix, actualPrefix); + } + catch (Exception ex) + { + logger.LogWarning($"Failed to add service spec {fullSpecUrl} to swagger doc: {ex.Message}"); + return (null, string.Empty, string.Empty, string.Empty); + } + } + + private void AddServiceSpec(OpenApiDocument doc, OpenApiDocument spec, string specUrl, string bffPrefix, string actualPrefix) + { + try + { + logger.LogInformation($"Adding service spec {specUrl} to swagger doc"); foreach (var schema in spec.Components.Schemas) { @@ -65,15 +80,16 @@ private async Task AddServiceSpec(OpenApiDocument doc, string? serviceUrl, strin ? path.Key.Replace(actualPrefix, bffPrefix) : path.Key; - logger.LogDebug($"Adding path {newPath} (originally {path.Key} from {serviceUrl}"); + logger.LogDebug($"Adding path {newPath} (originally {path.Key} from {specUrl}"); // Assumes that the proxy path of the service is the same as the paths in the service spec - doc.Paths.Add(newPath, path.Value); + if (!doc.Paths.ContainsKey(newPath)) + doc.Paths.Add(newPath, path.Value); } } catch (Exception ex) { - logger.LogWarning($"Failed to add service spec {fullSpecUrl} to swagger doc: {ex.Message}"); + logger.LogWarning($"Failed to add service spec {specUrl} to swagger doc: {ex.Message}"); } } @@ -183,22 +199,43 @@ public static string GetFullSpecUrl(string serviceUrl, string specUrl) public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) { - var tasks = new List - { - AddServiceSpec(swaggerDoc, serviceRegistry.Value.AccountServiceUrl, config.Value.AccountServiceSpecUrl, config.Value.AccountServiceBffPrefix, config.Value.AccountServiceActualPrefix), - AddServiceSpec(swaggerDoc, serviceRegistry.Value.AuditServiceUrl, config.Value.AuditServiceSpecUrl, config.Value.AuditServiceBffPrefix, config.Value.AuditServiceActualPrefix), - AddServiceSpec(swaggerDoc, serviceRegistry.Value.CensusServiceUrl, config.Value.CensusServiceSpecUrl, config.Value.CensusServiceBffPrefix, config.Value.CensusServiceActualPrefix), - AddServiceSpec(swaggerDoc, serviceRegistry.Value.DataAcquisitionServiceUrl, config.Value.DataAcquisitionServiceSpecUrl, config.Value.DataAcquisitionServiceBffPrefix, config.Value.DataAcquisitionServiceActualPrefix), - AddServiceSpec(swaggerDoc, serviceRegistry.Value.MeasureServiceUrl, config.Value.MeasureServiceSpecUrl, config.Value.MeasureServiceBffPrefix, config.Value.MeasureServiceActualPrefix), - AddServiceSpec(swaggerDoc, serviceRegistry.Value.NormalizationServiceUrl, config.Value.NormalizationServiceSpecUrl, config.Value.NormalizationServiceBffPrefix, config.Value.NormalizationServiceActualPrefix), - AddServiceSpec(swaggerDoc, serviceRegistry.Value.NotificationServiceUrl, config.Value.NotificationServiceSpecUrl, config.Value.NotificationServiceBffPrefix, config.Value.NotificationServiceActualPrefix), - AddServiceSpec(swaggerDoc, serviceRegistry.Value.QueryDispatchServiceUrl, config.Value.QueryDispatchServiceSpecUrl, config.Value.QueryDispatchServiceBffPrefix, config.Value.QueryDispatchServiceActualPrefix), - AddServiceSpec(swaggerDoc, serviceRegistry.Value.ReportServiceUrl, config.Value.ReportServiceSpecUrl, config.Value.ReportServiceBffPrefix, config.Value.ReportServiceActualPrefix), - AddServiceSpec(swaggerDoc, serviceRegistry.Value.SubmissionServiceUrl, config.Value.SubmissionServiceSpecUrl, config.Value.SubmissionServiceBffPrefix, config.Value.SubmissionServiceActualPrefix), - AddServiceSpec(swaggerDoc, serviceRegistry.Value.TenantService.TenantServiceUrl, config.Value.TenantServiceSpecUrl, config.Value.TenantServiceBffPrefix, config.Value.TenantServiceActualPrefix), - AddServiceSpec(swaggerDoc, serviceRegistry.Value.ValidationServiceUrl, config.Value.ValidationServiceSpecUrl, config.Value.ValidationServiceBffPrefix, config.Value.ValidationServiceActualPrefix) + // Update tags for already-existing operations to be prefixed with "BFF - " + foreach (var path in swaggerDoc.Paths) + { + foreach (var operation in path.Value.Operations) + { + foreach (var tag in operation.Value.Tags) + { + if (!string.IsNullOrEmpty(tag.Name) && !tag.Name.StartsWith("Admin.BFF")) + tag.Name = $"Admin.BFF - {tag.Name}"; + else if (string.IsNullOrEmpty(tag.Name)) + tag.Name = "Admin.BFF - General"; + } + } + } + + var tasks = new List> + { + GetServiceSpec(serviceRegistry.Value.AccountServiceUrl, config.Value.AccountServiceSpecUrl, config.Value.AccountServiceBffPrefix, config.Value.AccountServiceActualPrefix), + GetServiceSpec(serviceRegistry.Value.AuditServiceUrl, config.Value.AuditServiceSpecUrl, config.Value.AuditServiceBffPrefix, config.Value.AuditServiceActualPrefix), + GetServiceSpec(serviceRegistry.Value.CensusServiceUrl, config.Value.CensusServiceSpecUrl, config.Value.CensusServiceBffPrefix, config.Value.CensusServiceActualPrefix), + GetServiceSpec(serviceRegistry.Value.DataAcquisitionServiceUrl, config.Value.DataAcquisitionServiceSpecUrl, config.Value.DataAcquisitionServiceBffPrefix, config.Value.DataAcquisitionServiceActualPrefix), + GetServiceSpec(serviceRegistry.Value.MeasureServiceUrl, config.Value.MeasureServiceSpecUrl, config.Value.MeasureServiceBffPrefix, config.Value.MeasureServiceActualPrefix), + GetServiceSpec(serviceRegistry.Value.NormalizationServiceUrl, config.Value.NormalizationServiceSpecUrl, config.Value.NormalizationServiceBffPrefix, config.Value.NormalizationServiceActualPrefix), + GetServiceSpec(serviceRegistry.Value.NotificationServiceUrl, config.Value.NotificationServiceSpecUrl, config.Value.NotificationServiceBffPrefix, config.Value.NotificationServiceActualPrefix), + GetServiceSpec(serviceRegistry.Value.QueryDispatchServiceUrl, config.Value.QueryDispatchServiceSpecUrl, config.Value.QueryDispatchServiceBffPrefix, config.Value.QueryDispatchServiceActualPrefix), + GetServiceSpec(serviceRegistry.Value.ReportServiceUrl, config.Value.ReportServiceSpecUrl, config.Value.ReportServiceBffPrefix, config.Value.ReportServiceActualPrefix), + GetServiceSpec(serviceRegistry.Value.SubmissionServiceUrl, config.Value.SubmissionServiceSpecUrl, config.Value.SubmissionServiceBffPrefix, config.Value.SubmissionServiceActualPrefix), + GetServiceSpec(serviceRegistry.Value.TenantService.TenantServiceUrl, config.Value.TenantServiceSpecUrl, config.Value.TenantServiceBffPrefix, config.Value.TenantServiceActualPrefix), + GetServiceSpec(serviceRegistry.Value.ValidationServiceUrl, config.Value.ValidationServiceSpecUrl, config.Value.ValidationServiceBffPrefix, config.Value.ValidationServiceActualPrefix) }; - Task.WhenAll(tasks).GetAwaiter().GetResult(); + var results = Task.WhenAll(tasks).GetAwaiter().GetResult(); + + foreach ((OpenApiDocument spec, string specUrl, string bffPrefix, string actualPrefix) in results) + { + if (spec == null) continue; + AddServiceSpec(swaggerDoc, spec, specUrl, bffPrefix, actualPrefix); + } } } \ No newline at end of file diff --git a/DotNet/Admin.BFF/Infrastructure/Filters/YarpConfigFilter.cs b/DotNet/Admin.BFF/Infrastructure/Filters/YarpConfigFilter.cs index 653f8af23..c43f8e005 100644 --- a/DotNet/Admin.BFF/Infrastructure/Filters/YarpConfigFilter.cs +++ b/DotNet/Admin.BFF/Infrastructure/Filters/YarpConfigFilter.cs @@ -19,7 +19,6 @@ public ValueTask ConfigureClusterAsync(ClusterConfig origCluster, { var newDests = new Dictionary(StringComparer.OrdinalIgnoreCase); - string endpoint = origCluster.ClusterId switch { "AccountService" => _serviceRegistry.AccountServiceUrl ?? string.Empty, @@ -33,6 +32,7 @@ public ValueTask ConfigureClusterAsync(ClusterConfig origCluster, "ReportService" => _serviceRegistry.ReportServiceUrl ?? string.Empty, "SubmissionService" => _serviceRegistry.SubmissionServiceUrl ?? string.Empty, "TenantService" => _serviceRegistry.TenantService.TenantServiceUrl ?? string.Empty, + "ValidationService" => _serviceRegistry.ValidationServiceUrl ?? string.Empty, _ => string.Empty }; diff --git a/docs/service_specs/account.md b/docs/service_specs/account.md index 8bb476511..349b8bb5b 100644 --- a/docs/service_specs/account.md +++ b/docs/service_specs/account.md @@ -65,3 +65,7 @@ The **Account** service provides REST endpoints for managing users, roles, and c ### Claims Management - **GET /api/account/claims**: Retrieve a list of all assignable claims. + +## Development Notes + +The service uses the "minimal endpoint" pattern. Endpoints are defined in the Presentation/Endpoints folder. \ No newline at end of file