merge(furniture-service): integrate furniture-service-orchestration-normalization

This commit is contained in:
José René White Enciso 2026-02-25 14:42:02 -06:00
commit e589338aa5
17 changed files with 341 additions and 57 deletions

View File

@ -0,0 +1,14 @@
# Furniture Service Orchestration Boundary
## Purpose
Constrain furniture-service to orchestration responsibilities after domain extraction.
## Service Responsibilities
- Coordinate use-case flow
- Call domain abstractions for decisions
- Adapt transport contracts
- Depend on Domain and DAL boundaries without direct BuildingBlock ownership
## Prohibited Responsibilities
- Owning business decision rules
- Owning persistence decision concerns

View File

@ -0,0 +1,10 @@
# Domain Delegation Plan
## Delegation Model
- Use cases invoke furniture-domain abstractions for decision logic.
- Service adapters retain technical mapping responsibilities.
## Transition Steps
1. Replace in-service decision branches with domain calls.
2. Keep contract shapes stable at service boundary.
3. Validate orchestration-only responsibilities.

View File

@ -0,0 +1,10 @@
# Orchestration Regression Checks
## Checks
- Service no longer contains domain decision branches.
- Service still orchestrates required dependencies.
- Transport contract outputs remain stable.
## Evidence
- Updated architecture docs
- Migration mapping confirmation

View File

@ -0,0 +1,36 @@
using Furniture.Service.Application.Grpc;
using Furniture.Service.Contracts.UseCases;
namespace Furniture.Service.Application.Adapters;
/// <summary>
/// Default adapter implementation for furniture service gRPC contract translation.
/// </summary>
public sealed class FurnitureAvailabilityGrpcContractAdapter : IFurnitureAvailabilityGrpcContractAdapter
{
/// <inheritdoc />
public GetFurnitureAvailabilityGrpcContract ToGrpc(GetFurnitureAvailabilityRequest request)
{
return new GetFurnitureAvailabilityGrpcContract(
request.FurnitureId,
ResolveCorrelationId(request.CorrelationId));
}
/// <inheritdoc />
public GetFurnitureAvailabilityRequest FromGrpc(GetFurnitureAvailabilityGrpcContract contract)
{
return new GetFurnitureAvailabilityRequest(
contract.FurnitureId,
ResolveCorrelationId(contract.CorrelationId));
}
private static string ResolveCorrelationId(string correlationId)
{
if (!string.IsNullOrWhiteSpace(correlationId))
{
return correlationId;
}
return $"corr-{Guid.NewGuid():N}";
}
}

View File

@ -1,39 +0,0 @@
using BuildingBlock.Catalog.Contracts.Products;
using BuildingBlock.Catalog.Contracts.Responses;
using BuildingBlock.Inventory.Contracts.Requests;
using BuildingBlock.Inventory.Contracts.Responses;
using Furniture.Service.Contracts.UseCases;
namespace Furniture.Service.Application.Adapters;
/// <summary>
/// Defines adapter boundary for furniture service contract composition.
/// </summary>
public interface IFurnitureAvailabilityContractAdapter
{
/// <summary>
/// Maps service request into an inventory capability request.
/// </summary>
/// <param name="request">Furniture availability request.</param>
/// <returns>Inventory lookup request.</returns>
InventoryItemLookupRequest ToInventoryRequest(GetFurnitureAvailabilityRequest request);
/// <summary>
/// Maps service request into a catalog capability request.
/// </summary>
/// <param name="request">Furniture availability request.</param>
/// <returns>Catalog product request.</returns>
ProductContract ToCatalogRequest(GetFurnitureAvailabilityRequest request);
/// <summary>
/// Maps capability responses back into the service response contract.
/// </summary>
/// <param name="request">Furniture availability request.</param>
/// <param name="catalogResponse">Catalog product response.</param>
/// <param name="inventoryResponse">Inventory lookup response.</param>
/// <returns>Furniture availability response.</returns>
GetFurnitureAvailabilityResponse ToServiceResponse(
GetFurnitureAvailabilityRequest request,
ProductContractResponse catalogResponse,
InventoryItemLookupResponse inventoryResponse);
}

View File

@ -0,0 +1,35 @@
using Core.Blueprint.Common.DependencyInjection;
using Furniture.DAL.DependencyInjection;
using Furniture.Domain.Decisions;
using Furniture.Service.Application.Adapters;
using Furniture.Service.Application.Ports;
using Furniture.Service.Application.UseCases;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace Furniture.Service.Application.DependencyInjection;
/// <summary>
/// Registers furniture-service runtime orchestration and DAL adapters.
/// </summary>
public static class FurnitureServiceRuntimeServiceCollectionExtensions
{
/// <summary>
/// Adds furniture-service runtime wiring aligned with blueprint runtime and furniture-dal runtime.
/// </summary>
/// <param name="services">Service collection.</param>
/// <returns>Service collection for fluent chaining.</returns>
public static IServiceCollection AddFurnitureServiceRuntime(this IServiceCollection services)
{
services.AddBlueprintRuntimeCore();
services.AddFurnitureDalRuntime();
services.TryAddSingleton<IFurnitureAvailabilityDecisionService, FurnitureAvailabilityDecisionService>();
services.TryAddSingleton<IFurnitureAvailabilityGrpcContractAdapter, FurnitureAvailabilityGrpcContractAdapter>();
services.TryAddSingleton<IFurnitureAvailabilityReadPort, FurnitureAvailabilityReadPortDalAdapter>();
services.TryAddSingleton<ICatalogProductReadPort, CatalogProductReadPortDalAdapter>();
services.TryAddSingleton<IGetFurnitureAvailabilityUseCase, GetFurnitureAvailabilityUseCase>();
return services;
}
}

View File

@ -5,8 +5,9 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\..\building-block-catalog\src\BuildingBlock.Catalog.Contracts\BuildingBlock.Catalog.Contracts.csproj" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
<ProjectReference Include="..\..\..\building-block-inventory\src\BuildingBlock.Inventory.Contracts\BuildingBlock.Inventory.Contracts.csproj" /> <ProjectReference Include="..\..\..\furniture-domain\src\Furniture.Domain\Furniture.Domain.csproj" />
<ProjectReference Include="..\..\..\furniture-dal\src\Furniture.DAL\Furniture.DAL.csproj" />
<ProjectReference Include="..\Furniture.Service.Contracts\Furniture.Service.Contracts.csproj" /> <ProjectReference Include="..\Furniture.Service.Contracts\Furniture.Service.Contracts.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -0,0 +1,38 @@
using BuildingBlock.Catalog.Contracts.Conventions;
using BuildingBlock.Catalog.Contracts.Products;
using BuildingBlock.Catalog.Contracts.Responses;
using Furniture.DAL.Contracts;
using Furniture.DAL.Repositories;
namespace Furniture.Service.Application.Ports;
/// <summary>
/// Default DAL adapter for catalog product read port.
/// </summary>
public sealed class CatalogProductReadPortDalAdapter(ICatalogRepository catalogRepository) : ICatalogProductReadPort
{
/// <inheritdoc />
public async Task<ProductContractResponse> ReadProductAsync(ProductContract request)
{
var lookupRequest = new CatalogProductLookupRequest(
new FurnitureDalContractEnvelope(
request.Envelope.ContractVersion,
request.Envelope.CorrelationId),
request.ProductId);
var projectionRecord = await catalogRepository.ReadProductAsync(lookupRequest);
if (projectionRecord is null)
{
return new ProductContractResponse(request.Envelope, request.ProductId, string.Empty);
}
var responseEnvelope = new CatalogContractEnvelope(
projectionRecord.Envelope.ContractVersion,
projectionRecord.Envelope.CorrelationId);
return new ProductContractResponse(
responseEnvelope,
projectionRecord.ProductId,
projectionRecord.DisplayName);
}
}

View File

@ -0,0 +1,39 @@
using BuildingBlock.Inventory.Contracts.Conventions;
using BuildingBlock.Inventory.Contracts.Requests;
using BuildingBlock.Inventory.Contracts.Responses;
using Furniture.DAL.Contracts;
using Furniture.DAL.Repositories;
namespace Furniture.Service.Application.Ports;
/// <summary>
/// Default DAL adapter for furniture availability read port.
/// </summary>
public sealed class FurnitureAvailabilityReadPortDalAdapter(
IFurnitureRepository furnitureRepository) : IFurnitureAvailabilityReadPort
{
/// <inheritdoc />
public async Task<InventoryItemLookupResponse> ReadAvailabilityAsync(InventoryItemLookupRequest request)
{
var lookupRequest = new FurnitureAvailabilityLookupRequest(
new FurnitureDalContractEnvelope(
request.Envelope.ContractVersion,
request.Envelope.CorrelationId),
request.ItemCode);
var availabilityRecord = await furnitureRepository.ReadAvailabilityAsync(lookupRequest);
if (availabilityRecord is null)
{
return new InventoryItemLookupResponse(request.Envelope, request.ItemCode, 0);
}
var responseEnvelope = new InventoryContractEnvelope(
availabilityRecord.Envelope.ContractVersion,
availabilityRecord.Envelope.CorrelationId);
return new InventoryItemLookupResponse(
responseEnvelope,
availabilityRecord.FurnitureId,
availabilityRecord.QuantityAvailable);
}
}

View File

@ -1,6 +1,7 @@
using Furniture.Service.Application.Adapters;
using Furniture.Service.Application.Ports; using Furniture.Service.Application.Ports;
using Furniture.Service.Contracts.UseCases; using Furniture.Service.Contracts.UseCases;
using Furniture.Domain.Contracts;
using Furniture.Domain.Decisions;
namespace Furniture.Service.Application.UseCases; namespace Furniture.Service.Application.UseCases;
@ -8,7 +9,7 @@ namespace Furniture.Service.Application.UseCases;
/// Default orchestration implementation for furniture availability lookup. /// Default orchestration implementation for furniture availability lookup.
/// </summary> /// </summary>
public sealed class GetFurnitureAvailabilityUseCase( public sealed class GetFurnitureAvailabilityUseCase(
IFurnitureAvailabilityContractAdapter contractAdapter, IFurnitureAvailabilityDecisionService decisionService,
ICatalogProductReadPort catalogReadPort, ICatalogProductReadPort catalogReadPort,
IFurnitureAvailabilityReadPort inventoryReadPort) IFurnitureAvailabilityReadPort inventoryReadPort)
: IGetFurnitureAvailabilityUseCase : IGetFurnitureAvailabilityUseCase
@ -16,17 +17,25 @@ public sealed class GetFurnitureAvailabilityUseCase(
/// <inheritdoc /> /// <inheritdoc />
public async Task<GetFurnitureAvailabilityResponse> HandleAsync(GetFurnitureAvailabilityRequest request) public async Task<GetFurnitureAvailabilityResponse> HandleAsync(GetFurnitureAvailabilityRequest request)
{ {
var catalogRequest = contractAdapter.ToCatalogRequest(request); var domainRequest = new FurnitureAvailabilityDecisionRequest(
var inventoryRequest = contractAdapter.ToInventoryRequest(request); request.FurnitureId,
request.CorrelationId);
var catalogRequest = decisionService.BuildCatalogRequest(domainRequest);
var inventoryRequest = decisionService.BuildInventoryRequest(domainRequest);
var catalogTask = catalogReadPort.ReadProductAsync(catalogRequest); var catalogTask = catalogReadPort.ReadProductAsync(catalogRequest);
var inventoryTask = inventoryReadPort.ReadAvailabilityAsync(inventoryRequest); var inventoryTask = inventoryReadPort.ReadAvailabilityAsync(inventoryRequest);
await Task.WhenAll(catalogTask, inventoryTask); await Task.WhenAll(catalogTask, inventoryTask);
return contractAdapter.ToServiceResponse( var domainResponse = decisionService.ComposeResponse(
request, domainRequest,
await catalogTask, await catalogTask,
await inventoryTask); await inventoryTask);
return new GetFurnitureAvailabilityResponse(
domainResponse.FurnitureId,
domainResponse.DisplayName,
domainResponse.QuantityAvailable);
} }
} }

View File

@ -4,6 +4,16 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<PackageReference Include="Grpc.AspNetCore" 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="Protos\furniture_runtime.proto" GrpcServices="Server" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Furniture.Service.Application\Furniture.Service.Application.csproj" /> <ProjectReference Include="..\Furniture.Service.Application\Furniture.Service.Application.csproj" />
<ProjectReference Include="..\Furniture.Service.Contracts\Furniture.Service.Contracts.csproj" /> <ProjectReference Include="..\Furniture.Service.Contracts\Furniture.Service.Contracts.csproj" />

View File

@ -1,6 +1,15 @@
using Furniture.Service.Application.DependencyInjection;
using Furniture.Service.Grpc.Services;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
// Stage 3 skeleton: keep a single active internal protocol policy (gRPC-first). builder.Services.AddGrpc();
builder.Services.AddHealthChecks();
builder.Services.AddFurnitureServiceRuntime();
var app = builder.Build(); var app = builder.Build();
app.MapGrpcService<FurnitureRuntimeGrpcService>();
app.MapHealthChecks("/healthz");
app.Run(); app.Run();

View File

@ -0,0 +1,20 @@
syntax = "proto3";
option csharp_namespace = "Furniture.Service.Grpc";
package furniture.service.grpc;
service FurnitureRuntime {
rpc GetFurnitureAvailability (GetFurnitureAvailabilityGrpcRequest) returns (GetFurnitureAvailabilityGrpcResponse);
}
message GetFurnitureAvailabilityGrpcRequest {
string furniture_id = 1;
string correlation_id = 2;
}
message GetFurnitureAvailabilityGrpcResponse {
string furniture_id = 1;
string display_name = 2;
int32 quantity_available = 3;
}

View File

@ -0,0 +1,39 @@
using Grpc.Core;
using Furniture.Service.Application.Adapters;
using Furniture.Service.Application.Grpc;
using Furniture.Service.Application.UseCases;
namespace Furniture.Service.Grpc.Services;
/// <summary>
/// Internal gRPC endpoint implementation for furniture runtime operations.
/// </summary>
public sealed class FurnitureRuntimeGrpcService(
IGetFurnitureAvailabilityUseCase getFurnitureAvailabilityUseCase,
IFurnitureAvailabilityGrpcContractAdapter grpcContractAdapter) : FurnitureRuntime.FurnitureRuntimeBase
{
/// <summary>
/// Executes furniture availability lookup through service use-case orchestration.
/// </summary>
/// <param name="request">gRPC availability request.</param>
/// <param name="context">gRPC server call context.</param>
/// <returns>gRPC availability response.</returns>
public override async Task<GetFurnitureAvailabilityGrpcResponse> GetFurnitureAvailability(
GetFurnitureAvailabilityGrpcRequest request,
ServerCallContext context)
{
var grpcContract = new GetFurnitureAvailabilityGrpcContract(
request.FurnitureId,
request.CorrelationId);
var useCaseRequest = grpcContractAdapter.FromGrpc(grpcContract);
var useCaseResponse = await getFurnitureAvailabilityUseCase.HandleAsync(useCaseRequest);
return new GetFurnitureAvailabilityGrpcResponse
{
FurnitureId = useCaseResponse.FurnitureId,
DisplayName = useCaseResponse.DisplayName,
QuantityAvailable = useCaseResponse.QuantityAvailable
};
}
}

View File

@ -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" />

View File

@ -4,10 +4,11 @@ using BuildingBlock.Catalog.Contracts.Responses;
using BuildingBlock.Inventory.Contracts.Conventions; using BuildingBlock.Inventory.Contracts.Conventions;
using BuildingBlock.Inventory.Contracts.Requests; using BuildingBlock.Inventory.Contracts.Requests;
using BuildingBlock.Inventory.Contracts.Responses; using BuildingBlock.Inventory.Contracts.Responses;
using Furniture.Service.Application.Adapters;
using Furniture.Service.Application.Ports; using Furniture.Service.Application.Ports;
using Furniture.Service.Application.UseCases; using Furniture.Service.Application.UseCases;
using Furniture.Service.Contracts.UseCases; using Furniture.Service.Contracts.UseCases;
using Furniture.Domain.Contracts;
using Furniture.Domain.Decisions;
namespace Furniture.Service.Application.UnitTests; namespace Furniture.Service.Application.UnitTests;
@ -16,10 +17,10 @@ public class GetFurnitureAvailabilityUseCaseTests
[Fact] [Fact]
public async Task HandleAsync_WhenCalled_DelegatesToReadPort() public async Task HandleAsync_WhenCalled_DelegatesToReadPort()
{ {
var adapter = new FakeFurnitureAvailabilityContractAdapter(); var decisionService = new FakeFurnitureAvailabilityDecisionService();
var catalogPort = new FakeCatalogProductReadPort(); var catalogPort = new FakeCatalogProductReadPort();
var inventoryPort = new FakeFurnitureAvailabilityReadPort(); var inventoryPort = new FakeFurnitureAvailabilityReadPort();
var useCase = new GetFurnitureAvailabilityUseCase(adapter, catalogPort, inventoryPort); var useCase = new GetFurnitureAvailabilityUseCase(decisionService, catalogPort, inventoryPort);
var response = await useCase.HandleAsync(new GetFurnitureAvailabilityRequest("FUR-001", "corr-123")); var response = await useCase.HandleAsync(new GetFurnitureAvailabilityRequest("FUR-001", "corr-123"));
@ -28,9 +29,9 @@ public class GetFurnitureAvailabilityUseCaseTests
Assert.Equal(10, response.QuantityAvailable); Assert.Equal(10, response.QuantityAvailable);
} }
private sealed class FakeFurnitureAvailabilityContractAdapter : IFurnitureAvailabilityContractAdapter private sealed class FakeFurnitureAvailabilityDecisionService : IFurnitureAvailabilityDecisionService
{ {
public ProductContract ToCatalogRequest(GetFurnitureAvailabilityRequest request) public ProductContract BuildCatalogRequest(FurnitureAvailabilityDecisionRequest request)
{ {
return new ProductContract( return new ProductContract(
new CatalogContractEnvelope("1.0.0", request.CorrelationId), new CatalogContractEnvelope("1.0.0", request.CorrelationId),
@ -38,19 +39,19 @@ public class GetFurnitureAvailabilityUseCaseTests
string.Empty); string.Empty);
} }
public InventoryItemLookupRequest ToInventoryRequest(GetFurnitureAvailabilityRequest request) public InventoryItemLookupRequest BuildInventoryRequest(FurnitureAvailabilityDecisionRequest request)
{ {
return new InventoryItemLookupRequest( return new InventoryItemLookupRequest(
new InventoryContractEnvelope("1.0.0", request.CorrelationId), new InventoryContractEnvelope("1.0.0", request.CorrelationId),
request.FurnitureId); request.FurnitureId);
} }
public GetFurnitureAvailabilityResponse ToServiceResponse( public FurnitureAvailabilityDecisionResponse ComposeResponse(
GetFurnitureAvailabilityRequest request, FurnitureAvailabilityDecisionRequest request,
ProductContractResponse catalogResponse, ProductContractResponse catalogResponse,
InventoryItemLookupResponse inventoryResponse) InventoryItemLookupResponse inventoryResponse)
{ {
return new GetFurnitureAvailabilityResponse( return new FurnitureAvailabilityDecisionResponse(
request.FurnitureId, request.FurnitureId,
catalogResponse.DisplayName, catalogResponse.DisplayName,
inventoryResponse.QuantityAvailable); inventoryResponse.QuantityAvailable);

View File

@ -0,0 +1,51 @@
using Furniture.Service.Application.Adapters;
using Furniture.Service.Application.DependencyInjection;
using Furniture.Service.Application.Grpc;
using Furniture.Service.Application.UseCases;
using Furniture.Service.Contracts.UseCases;
using Microsoft.Extensions.DependencyInjection;
namespace Furniture.Service.Application.UnitTests;
public class RuntimeWiringTests
{
[Fact]
public async Task AddFurnitureServiceRuntime_WhenInvoked_ResolvesUseCase()
{
var services = new ServiceCollection();
services.AddFurnitureServiceRuntime();
using var provider = services.BuildServiceProvider();
var useCase = provider.GetRequiredService<IGetFurnitureAvailabilityUseCase>();
var response = await useCase.HandleAsync(new GetFurnitureAvailabilityRequest("FUR-001", "corr-123"));
Assert.Equal("FUR-001", response.FurnitureId);
Assert.Equal(8, response.QuantityAvailable);
}
[Fact]
public void FurnitureAvailabilityGrpcContractAdapter_WhenMapped_PreservesValues()
{
var adapter = new FurnitureAvailabilityGrpcContractAdapter();
var request = new GetFurnitureAvailabilityRequest("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 FurnitureAvailabilityGrpcContractAdapter_WhenCorrelationMissing_GeneratesCorrelation()
{
var adapter = new FurnitureAvailabilityGrpcContractAdapter();
var grpcContract = new GetFurnitureAvailabilityGrpcContract("FUR-003", string.Empty);
var mapped = adapter.FromGrpc(grpcContract);
Assert.Equal("FUR-003", mapped.FurnitureId);
Assert.NotEmpty(mapped.CorrelationId);
}
}