merge(furniture-bff): integrate furniture-bff-edge-policy
This commit is contained in:
commit
ac4c7b291c
14
docs/architecture/bff-service-boundary.md
Normal file
14
docs/architecture/bff-service-boundary.md
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# Furniture BFF Service Boundary
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
Preserve BFF as an edge adapter layer that depends on service contracts only.
|
||||||
|
|
||||||
|
## BFF Responsibilities
|
||||||
|
- REST edge exposure
|
||||||
|
- Service client adaptation
|
||||||
|
- Correlation/tracing propagation
|
||||||
|
- Single active edge protocol policy enforcement (`rest`)
|
||||||
|
|
||||||
|
## Prohibited
|
||||||
|
- Direct DAL access
|
||||||
|
- Domain business decision ownership
|
||||||
6
docs/migration/correlation-propagation-checks.md
Normal file
6
docs/migration/correlation-propagation-checks.md
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# Correlation Propagation Checks
|
||||||
|
|
||||||
|
## Checks
|
||||||
|
- Correlation identifiers are preserved across BFF -> Service calls.
|
||||||
|
- No new correlation behavior is introduced at edge.
|
||||||
|
- Trace metadata pass-through remains stable.
|
||||||
9
docs/migration/post-domain-contract-alignment.md
Normal file
9
docs/migration/post-domain-contract-alignment.md
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# Post-Domain Contract Alignment
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
Align BFF adapter usage with service contracts after domain extraction.
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
1. Map current BFF contract usage to updated service contracts.
|
||||||
|
2. Keep edge contract behavior stable.
|
||||||
|
3. Validate adapter compatibility.
|
||||||
@ -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>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
|
||||||
<ProjectReference Include="..\Furniture.Bff.Contracts\Furniture.Bff.Contracts.csproj" />
|
<ProjectReference Include="..\Furniture.Bff.Contracts\Furniture.Bff.Contracts.csproj" />
|
||||||
<ProjectReference Include="..\..\..\furniture-service\src\Furniture.Service.Contracts\Furniture.Service.Contracts.csproj" />
|
<ProjectReference Include="..\..\..\furniture-service\src\Furniture.Service.Contracts\Furniture.Service.Contracts.csproj" />
|
||||||
</ItemGroup>
|
</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>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
</PropertyGroup>
|
</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>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Furniture.Bff.Application\Furniture.Bff.Application.csproj" />
|
<ProjectReference Include="..\Furniture.Bff.Application\Furniture.Bff.Application.csproj" />
|
||||||
<ProjectReference Include="..\Furniture.Bff.Contracts\Furniture.Bff.Contracts.csproj" />
|
<ProjectReference Include="..\Furniture.Bff.Contracts\Furniture.Bff.Contracts.csproj" />
|
||||||
|
<ProjectReference Include="..\..\..\blueprint-platform\src\Core.Blueprint.Common\Core.Blueprint.Common.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@ -1,13 +1,76 @@
|
|||||||
|
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.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);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
var edgeProtocol = builder.Configuration["FurnitureBff:EdgeProtocol"] ?? "rest";
|
||||||
// Stage 3 skeleton: single active external protocol for this deployment is REST.
|
if (!string.Equals(edgeProtocol, "rest", StringComparison.OrdinalIgnoreCase))
|
||||||
var app = builder.Build();
|
|
||||||
|
|
||||||
app.MapGet("/api/furniture/{furnitureId}/availability", (string furnitureId) =>
|
|
||||||
{
|
{
|
||||||
return Results.Ok(new GetFurnitureAvailabilityApiResponse(furnitureId, string.Empty, 0));
|
throw new InvalidOperationException(
|
||||||
|
$"Furniture BFF supports one active edge protocol per deployment. Configured: '{edgeProtocol}'. Expected: 'rest'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Services.AddHttpContextAccessor();
|
||||||
|
builder.Services.AddHealthChecks();
|
||||||
|
builder.Services.AddBlueprintRuntimeCore();
|
||||||
|
builder.Services.AddFurnitureBffApplicationRuntime();
|
||||||
|
builder.Services.AddScoped<IFurnitureServiceClient, FurnitureServiceGrpcClientAdapter>();
|
||||||
|
builder.Services.AddGrpcClient<FurnitureRuntime.FurnitureRuntimeClient>(options =>
|
||||||
|
{
|
||||||
|
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();
|
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>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
<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="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||||
<PackageReference Include="xunit" Version="2.9.3" />
|
<PackageReference Include="xunit" Version="2.9.3" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
|
<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