feat(furniture-bff): wire rest runtime and grpc client adapter
This commit is contained in:
parent
cbafd039c3
commit
fd2d61701d
@ -0,0 +1,37 @@
|
||||
using Furniture.Bff.Contracts.Api;
|
||||
using Furniture.Service.Contracts.UseCases;
|
||||
|
||||
namespace Furniture.Bff.Application.Adapters;
|
||||
|
||||
/// <summary>
|
||||
/// Default adapter implementation between furniture BFF edge and furniture service contracts.
|
||||
/// </summary>
|
||||
public sealed class FurnitureAvailabilityEdgeContractAdapter : IFurnitureAvailabilityEdgeContractAdapter
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public GetFurnitureAvailabilityRequest ToServiceRequest(GetFurnitureAvailabilityApiRequest request)
|
||||
{
|
||||
return new GetFurnitureAvailabilityRequest(
|
||||
request.FurnitureId,
|
||||
ResolveCorrelationId(request.CorrelationId));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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}";
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
using Furniture.Bff.Application.Grpc;
|
||||
using Furniture.Bff.Contracts.Api;
|
||||
|
||||
namespace Furniture.Bff.Application.Adapters;
|
||||
|
||||
/// <summary>
|
||||
/// Default adapter implementation for furniture edge gRPC contract translation.
|
||||
/// </summary>
|
||||
public sealed class FurnitureAvailabilityEdgeGrpcContractAdapter : IFurnitureAvailabilityEdgeGrpcContractAdapter
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public GetFurnitureAvailabilityEdgeGrpcContract ToGrpc(GetFurnitureAvailabilityApiRequest request)
|
||||
{
|
||||
return new GetFurnitureAvailabilityEdgeGrpcContract(
|
||||
request.FurnitureId,
|
||||
ResolveCorrelationId(request.CorrelationId));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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}";
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Registers application-layer runtime wiring for furniture-bff.
|
||||
/// </summary>
|
||||
public static class FurnitureBffApplicationServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds furniture-bff application handlers and adapter implementations.
|
||||
/// </summary>
|
||||
/// <param name="services">Service collection.</param>
|
||||
/// <returns>Service collection for fluent chaining.</returns>
|
||||
public static IServiceCollection AddFurnitureBffApplicationRuntime(this IServiceCollection services)
|
||||
{
|
||||
services.TryAddSingleton<IFurnitureAvailabilityEdgeContractAdapter, FurnitureAvailabilityEdgeContractAdapter>();
|
||||
services.TryAddSingleton<IFurnitureAvailabilityEdgeGrpcContractAdapter, FurnitureAvailabilityEdgeGrpcContractAdapter>();
|
||||
services.TryAddScoped<IGetFurnitureAvailabilityHandler, GetFurnitureAvailabilityHandler>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@ -5,6 +5,7 @@
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
|
||||
<ProjectReference Include="..\Furniture.Bff.Contracts\Furniture.Bff.Contracts.csproj" />
|
||||
<ProjectReference Include="..\..\..\furniture-service\src\Furniture.Service.Contracts\Furniture.Service.Contracts.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// gRPC-backed adapter for downstream furniture-service calls.
|
||||
/// </summary>
|
||||
public sealed class FurnitureServiceGrpcClientAdapter(
|
||||
FurnitureRuntime.FurnitureRuntimeClient grpcClient,
|
||||
IHttpContextAccessor httpContextAccessor) : IFurnitureServiceClient
|
||||
{
|
||||
private const string CorrelationHeaderName = "x-correlation-id";
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<GetFurnitureAvailabilityResponse> 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)
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -4,8 +4,22 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Google.Protobuf" Version="3.31.1" />
|
||||
<PackageReference Include="Grpc.Core.Api" Version="2.71.0" />
|
||||
<PackageReference Include="Grpc.Net.Client" Version="2.71.0" />
|
||||
<PackageReference Include="Grpc.Net.ClientFactory" Version="2.71.0" />
|
||||
<PackageReference Include="Grpc.Tools" Version="2.71.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Protobuf Include="..\..\..\furniture-service\src\Furniture.Service.Grpc\Protos\furniture_runtime.proto" GrpcServices="Client" Link="Protos\furniture_runtime.proto" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Furniture.Bff.Application\Furniture.Bff.Application.csproj" />
|
||||
<ProjectReference Include="..\Furniture.Bff.Contracts\Furniture.Bff.Contracts.csproj" />
|
||||
<ProjectReference Include="..\..\..\blueprint-platform\src\Core.Blueprint.Common\Core.Blueprint.Common.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@ -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<IFurnitureServiceClient, FurnitureServiceGrpcClientAdapter>();
|
||||
builder.Services.AddGrpcClient<FurnitureRuntime.FurnitureRuntimeClient>(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;
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
|
||||
|
||||
@ -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<IFurnitureServiceClient, FakeFurnitureServiceClient>();
|
||||
|
||||
using var provider = services.BuildServiceProvider();
|
||||
var handler = provider.GetRequiredService<IGetFurnitureAvailabilityHandler>();
|
||||
|
||||
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<GetFurnitureAvailabilityResponse> GetAvailabilityAsync(GetFurnitureAvailabilityRequest request)
|
||||
{
|
||||
return Task.FromResult(new GetFurnitureAvailabilityResponse(request.FurnitureId, "Chair", 7));
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user