From fd2d61701dc696df3607a516edcd9bcee0906411 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Ren=C3=A9=20White=20Enciso?= Date: Sun, 22 Feb 2026 19:26:50 -0600 Subject: [PATCH] feat(furniture-bff): wire rest runtime and grpc client adapter --- ...urnitureAvailabilityEdgeContractAdapter.cs | 37 ++++++++++ ...tureAvailabilityEdgeGrpcContractAdapter.cs | 36 ++++++++++ ...fApplicationServiceCollectionExtensions.cs | 26 +++++++ .../Furniture.Bff.Application.csproj | 1 + .../FurnitureServiceGrpcClientAdapter.cs | 70 +++++++++++++++++++ .../Furniture.Bff.Rest.csproj | 14 ++++ src/Furniture.Bff.Rest/Program.cs | 67 ++++++++++++++++-- ...Furniture.Bff.Application.UnitTests.csproj | 1 + .../RuntimeWiringTests.cs | 62 ++++++++++++++++ 9 files changed, 309 insertions(+), 5 deletions(-) create mode 100644 src/Furniture.Bff.Application/Adapters/FurnitureAvailabilityEdgeContractAdapter.cs create mode 100644 src/Furniture.Bff.Application/Adapters/FurnitureAvailabilityEdgeGrpcContractAdapter.cs create mode 100644 src/Furniture.Bff.Application/DependencyInjection/FurnitureBffApplicationServiceCollectionExtensions.cs create mode 100644 src/Furniture.Bff.Rest/Adapters/FurnitureServiceGrpcClientAdapter.cs create mode 100644 tests/Furniture.Bff.Application.UnitTests/RuntimeWiringTests.cs diff --git a/src/Furniture.Bff.Application/Adapters/FurnitureAvailabilityEdgeContractAdapter.cs b/src/Furniture.Bff.Application/Adapters/FurnitureAvailabilityEdgeContractAdapter.cs new file mode 100644 index 0000000..118d160 --- /dev/null +++ b/src/Furniture.Bff.Application/Adapters/FurnitureAvailabilityEdgeContractAdapter.cs @@ -0,0 +1,37 @@ +using Furniture.Bff.Contracts.Api; +using Furniture.Service.Contracts.UseCases; + +namespace Furniture.Bff.Application.Adapters; + +/// +/// Default adapter implementation between furniture BFF edge and furniture service contracts. +/// +public sealed class FurnitureAvailabilityEdgeContractAdapter : IFurnitureAvailabilityEdgeContractAdapter +{ + /// + public GetFurnitureAvailabilityRequest ToServiceRequest(GetFurnitureAvailabilityApiRequest request) + { + return new GetFurnitureAvailabilityRequest( + request.FurnitureId, + ResolveCorrelationId(request.CorrelationId)); + } + + /// + public GetFurnitureAvailabilityApiResponse ToApiResponse(GetFurnitureAvailabilityResponse response) + { + return new GetFurnitureAvailabilityApiResponse( + response.FurnitureId, + response.DisplayName, + response.QuantityAvailable); + } + + private static string ResolveCorrelationId(string correlationId) + { + if (!string.IsNullOrWhiteSpace(correlationId)) + { + return correlationId; + } + + return $"corr-{Guid.NewGuid():N}"; + } +} diff --git a/src/Furniture.Bff.Application/Adapters/FurnitureAvailabilityEdgeGrpcContractAdapter.cs b/src/Furniture.Bff.Application/Adapters/FurnitureAvailabilityEdgeGrpcContractAdapter.cs new file mode 100644 index 0000000..bd2e1e5 --- /dev/null +++ b/src/Furniture.Bff.Application/Adapters/FurnitureAvailabilityEdgeGrpcContractAdapter.cs @@ -0,0 +1,36 @@ +using Furniture.Bff.Application.Grpc; +using Furniture.Bff.Contracts.Api; + +namespace Furniture.Bff.Application.Adapters; + +/// +/// Default adapter implementation for furniture edge gRPC contract translation. +/// +public sealed class FurnitureAvailabilityEdgeGrpcContractAdapter : IFurnitureAvailabilityEdgeGrpcContractAdapter +{ + /// + public GetFurnitureAvailabilityEdgeGrpcContract ToGrpc(GetFurnitureAvailabilityApiRequest request) + { + return new GetFurnitureAvailabilityEdgeGrpcContract( + request.FurnitureId, + ResolveCorrelationId(request.CorrelationId)); + } + + /// + public GetFurnitureAvailabilityApiRequest FromGrpc(GetFurnitureAvailabilityEdgeGrpcContract contract) + { + return new GetFurnitureAvailabilityApiRequest( + contract.FurnitureId, + ResolveCorrelationId(contract.CorrelationId)); + } + + private static string ResolveCorrelationId(string correlationId) + { + if (!string.IsNullOrWhiteSpace(correlationId)) + { + return correlationId; + } + + return $"corr-{Guid.NewGuid():N}"; + } +} diff --git a/src/Furniture.Bff.Application/DependencyInjection/FurnitureBffApplicationServiceCollectionExtensions.cs b/src/Furniture.Bff.Application/DependencyInjection/FurnitureBffApplicationServiceCollectionExtensions.cs new file mode 100644 index 0000000..93d43b4 --- /dev/null +++ b/src/Furniture.Bff.Application/DependencyInjection/FurnitureBffApplicationServiceCollectionExtensions.cs @@ -0,0 +1,26 @@ +using Furniture.Bff.Application.Adapters; +using Furniture.Bff.Application.Handlers; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Furniture.Bff.Application.DependencyInjection; + +/// +/// Registers application-layer runtime wiring for furniture-bff. +/// +public static class FurnitureBffApplicationServiceCollectionExtensions +{ + /// + /// Adds furniture-bff application handlers and adapter implementations. + /// + /// Service collection. + /// Service collection for fluent chaining. + public static IServiceCollection AddFurnitureBffApplicationRuntime(this IServiceCollection services) + { + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddScoped(); + + return services; + } +} diff --git a/src/Furniture.Bff.Application/Furniture.Bff.Application.csproj b/src/Furniture.Bff.Application/Furniture.Bff.Application.csproj index 41b768c..85a3ebb 100644 --- a/src/Furniture.Bff.Application/Furniture.Bff.Application.csproj +++ b/src/Furniture.Bff.Application/Furniture.Bff.Application.csproj @@ -5,6 +5,7 @@ enable + diff --git a/src/Furniture.Bff.Rest/Adapters/FurnitureServiceGrpcClientAdapter.cs b/src/Furniture.Bff.Rest/Adapters/FurnitureServiceGrpcClientAdapter.cs new file mode 100644 index 0000000..d2f3600 --- /dev/null +++ b/src/Furniture.Bff.Rest/Adapters/FurnitureServiceGrpcClientAdapter.cs @@ -0,0 +1,70 @@ +using Furniture.Bff.Application.Adapters; +using Furniture.Service.Contracts.UseCases; +using Furniture.Service.Grpc; +using Grpc.Core; +using Microsoft.Extensions.Primitives; + +namespace Furniture.Bff.Rest.Adapters; + +/// +/// gRPC-backed adapter for downstream furniture-service calls. +/// +public sealed class FurnitureServiceGrpcClientAdapter( + FurnitureRuntime.FurnitureRuntimeClient grpcClient, + IHttpContextAccessor httpContextAccessor) : IFurnitureServiceClient +{ + private const string CorrelationHeaderName = "x-correlation-id"; + + /// + public async Task GetAvailabilityAsync(GetFurnitureAvailabilityRequest request) + { + var correlationId = ResolveCorrelationId(request.CorrelationId); + var grpcRequest = new GetFurnitureAvailabilityGrpcRequest + { + FurnitureId = request.FurnitureId, + CorrelationId = correlationId + }; + + var grpcResponse = await grpcClient.GetFurnitureAvailabilityAsync( + grpcRequest, + headers: CreateHeaders(correlationId), + deadline: DateTime.UtcNow.AddSeconds(10)); + + return new GetFurnitureAvailabilityResponse( + grpcResponse.FurnitureId, + grpcResponse.DisplayName, + grpcResponse.QuantityAvailable); + } + + private string ResolveCorrelationId(string? preferred = null) + { + if (!string.IsNullOrWhiteSpace(preferred)) + { + return preferred; + } + + var context = httpContextAccessor.HttpContext; + if (context?.Items.TryGetValue(CorrelationHeaderName, out var itemValue) == true && + itemValue is string itemCorrelationId && + !string.IsNullOrWhiteSpace(itemCorrelationId)) + { + return itemCorrelationId; + } + + if (context?.Request.Headers.TryGetValue(CorrelationHeaderName, out var headerValue) == true && + !StringValues.IsNullOrEmpty(headerValue)) + { + return headerValue.ToString(); + } + + return context?.TraceIdentifier ?? $"corr-{Guid.NewGuid():N}"; + } + + private static Metadata CreateHeaders(string correlationId) + { + return + [ + new Metadata.Entry(CorrelationHeaderName, correlationId) + ]; + } +} diff --git a/src/Furniture.Bff.Rest/Furniture.Bff.Rest.csproj b/src/Furniture.Bff.Rest/Furniture.Bff.Rest.csproj index 2c53814..c444ea1 100644 --- a/src/Furniture.Bff.Rest/Furniture.Bff.Rest.csproj +++ b/src/Furniture.Bff.Rest/Furniture.Bff.Rest.csproj @@ -4,8 +4,22 @@ enable enable + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + diff --git a/src/Furniture.Bff.Rest/Program.cs b/src/Furniture.Bff.Rest/Program.cs index 5dd0bbd..3edd947 100644 --- a/src/Furniture.Bff.Rest/Program.cs +++ b/src/Furniture.Bff.Rest/Program.cs @@ -1,13 +1,70 @@ +using Core.Blueprint.Common.DependencyInjection; +using Furniture.Bff.Application.Adapters; +using Furniture.Bff.Application.DependencyInjection; +using Furniture.Bff.Application.Handlers; using Furniture.Bff.Contracts.Api; +using Furniture.Bff.Rest.Adapters; +using Furniture.Bff.Rest.Endpoints; +using Furniture.Service.Grpc; +using Microsoft.Extensions.Primitives; + +const string CorrelationHeaderName = "x-correlation-id"; var builder = WebApplication.CreateBuilder(args); -// Stage 3 skeleton: single active external protocol for this deployment is REST. -var app = builder.Build(); - -app.MapGet("/api/furniture/{furnitureId}/availability", (string furnitureId) => +builder.Services.AddHttpContextAccessor(); +builder.Services.AddHealthChecks(); +builder.Services.AddBlueprintRuntimeCore(); +builder.Services.AddFurnitureBffApplicationRuntime(); +builder.Services.AddScoped(); +builder.Services.AddGrpcClient(options => { - return Results.Ok(new GetFurnitureAvailabilityApiResponse(furnitureId, string.Empty, 0)); + var serviceAddress = builder.Configuration["FurnitureService:GrpcAddress"] ?? "http://localhost:5252"; + options.Address = new Uri(serviceAddress); }); +var app = builder.Build(); + +app.Use(async (context, next) => +{ + var correlationId = ResolveCorrelationId(context); + context.Items[CorrelationHeaderName] = correlationId; + context.Request.Headers[CorrelationHeaderName] = correlationId; + context.Response.Headers[CorrelationHeaderName] = correlationId; + await next(); +}); + +app.MapGet($"{EndpointConventions.ApiPrefix}/{{furnitureId}}/availability", async ( + string furnitureId, + HttpContext context, + IGetFurnitureAvailabilityHandler handler) => +{ + var request = new GetFurnitureAvailabilityApiRequest( + furnitureId, + ResolveCorrelationId(context)); + + var response = await handler.HandleAsync(request); + return Results.Ok(response); +}); + +app.MapHealthChecks("/healthz"); + app.Run(); + +string ResolveCorrelationId(HttpContext context) +{ + if (context.Items.TryGetValue(CorrelationHeaderName, out var itemValue) && + itemValue is string itemCorrelationId && + !string.IsNullOrWhiteSpace(itemCorrelationId)) + { + return itemCorrelationId; + } + + if (context.Request.Headers.TryGetValue(CorrelationHeaderName, out var headerValue) && + !StringValues.IsNullOrEmpty(headerValue)) + { + return headerValue.ToString(); + } + + return context.TraceIdentifier; +} diff --git a/tests/Furniture.Bff.Application.UnitTests/Furniture.Bff.Application.UnitTests.csproj b/tests/Furniture.Bff.Application.UnitTests/Furniture.Bff.Application.UnitTests.csproj index 369c362..5092ef0 100644 --- a/tests/Furniture.Bff.Application.UnitTests/Furniture.Bff.Application.UnitTests.csproj +++ b/tests/Furniture.Bff.Application.UnitTests/Furniture.Bff.Application.UnitTests.csproj @@ -7,6 +7,7 @@ + diff --git a/tests/Furniture.Bff.Application.UnitTests/RuntimeWiringTests.cs b/tests/Furniture.Bff.Application.UnitTests/RuntimeWiringTests.cs new file mode 100644 index 0000000..6e2b68b --- /dev/null +++ b/tests/Furniture.Bff.Application.UnitTests/RuntimeWiringTests.cs @@ -0,0 +1,62 @@ +using Furniture.Bff.Application.Adapters; +using Furniture.Bff.Application.DependencyInjection; +using Furniture.Bff.Application.Grpc; +using Furniture.Bff.Application.Handlers; +using Furniture.Bff.Contracts.Api; +using Furniture.Service.Contracts.UseCases; +using Microsoft.Extensions.DependencyInjection; + +namespace Furniture.Bff.Application.UnitTests; + +public class RuntimeWiringTests +{ + [Fact] + public async Task AddFurnitureBffApplicationRuntime_WhenResolved_WiresHandler() + { + var services = new ServiceCollection(); + services.AddFurnitureBffApplicationRuntime(); + services.AddSingleton(); + + using var provider = services.BuildServiceProvider(); + var handler = provider.GetRequiredService(); + + var response = await handler.HandleAsync(new GetFurnitureAvailabilityApiRequest("FUR-001", "corr-123")); + + Assert.Equal("FUR-001", response.FurnitureId); + Assert.Equal("Chair", response.DisplayName); + Assert.Equal(7, response.QuantityAvailable); + } + + [Fact] + public void FurnitureAvailabilityEdgeGrpcContractAdapter_WhenMapped_PreservesValues() + { + var adapter = new FurnitureAvailabilityEdgeGrpcContractAdapter(); + var request = new GetFurnitureAvailabilityApiRequest("FUR-002", "corr-456"); + + var grpcContract = adapter.ToGrpc(request); + var roundtrip = adapter.FromGrpc(grpcContract); + + Assert.Equal("FUR-002", roundtrip.FurnitureId); + Assert.Equal("corr-456", roundtrip.CorrelationId); + } + + [Fact] + public void FurnitureAvailabilityEdgeGrpcContractAdapter_WhenCorrelationMissing_GeneratesCorrelation() + { + var adapter = new FurnitureAvailabilityEdgeGrpcContractAdapter(); + var grpcContract = new GetFurnitureAvailabilityEdgeGrpcContract("FUR-003", string.Empty); + + var mapped = adapter.FromGrpc(grpcContract); + + Assert.Equal("FUR-003", mapped.FurnitureId); + Assert.NotEmpty(mapped.CorrelationId); + } + + private sealed class FakeFurnitureServiceClient : IFurnitureServiceClient + { + public Task GetAvailabilityAsync(GetFurnitureAvailabilityRequest request) + { + return Task.FromResult(new GetFurnitureAvailabilityResponse(request.FurnitureId, "Chair", 7)); + } + } +}