chore(furniture-dal): merge contract integration branch

This commit is contained in:
José René White Enciso 2026-02-22 04:57:45 -06:00
commit 20cbe1bb3b
22 changed files with 310 additions and 0 deletions

View File

@ -2,14 +2,28 @@
skinparam packageStyle rectangle skinparam packageStyle rectangle
package "furniture-dal" { package "furniture-dal" {
class FurnitureAvailabilityLookupRequest
class FurnitureAvailabilityRecord
class CatalogProductLookupRequest
class CatalogProductProjectionRecord
interface IFurnitureDalGrpcContractAdapter
interface ICatalogProjectionContractAdapter
interface IFurnitureDataProvider interface IFurnitureDataProvider
interface ICatalogDataProvider interface ICatalogDataProvider
interface IFurnitureRepository interface IFurnitureRepository
interface ICatalogRepository interface ICatalogRepository
interface ICacheInvalidationPolicy interface ICacheInvalidationPolicy
IFurnitureDalGrpcContractAdapter --> FurnitureAvailabilityLookupRequest
IFurnitureDalGrpcContractAdapter --> CatalogProductLookupRequest
ICatalogProjectionContractAdapter --> CatalogProductLookupRequest
ICatalogProjectionContractAdapter --> CatalogProductProjectionRecord
IFurnitureRepository --> IFurnitureDataProvider IFurnitureRepository --> IFurnitureDataProvider
IFurnitureRepository --> FurnitureAvailabilityLookupRequest
IFurnitureRepository --> FurnitureAvailabilityRecord
ICatalogRepository --> ICatalogDataProvider ICatalogRepository --> ICatalogDataProvider
ICatalogRepository --> CatalogProductLookupRequest
ICatalogRepository --> CatalogProductProjectionRecord
IFurnitureRepository --> ICacheInvalidationPolicy IFurnitureRepository --> ICacheInvalidationPolicy
ICatalogRepository --> ICacheInvalidationPolicy ICatalogRepository --> ICacheInvalidationPolicy
} }

View File

@ -5,8 +5,10 @@
- Repositories coordinate provider calls for aggregate persistence. - Repositories coordinate provider calls for aggregate persistence.
- Cache invalidation policy is owned by DAL. - Cache invalidation policy is owned by DAL.
- Service layer does not implement persistence or cache invalidation details. - Service layer does not implement persistence or cache invalidation details.
- Cache invalidation boundaries are expressed through DAL contract interfaces.
## Policy ## Policy
- Persistence writes must define cache invalidation triggers. - Persistence writes must define cache invalidation triggers.
- Cache keys and invalidation behavior are centralized in DAL policy definitions. - Cache keys and invalidation behavior are centralized in DAL policy definitions.
- This stage defines contracts, ports, and adapters only; no persistence implementation is introduced.

View File

@ -4,9 +4,14 @@
- `IFurnitureDataProvider`: furniture persistence provider boundary. - `IFurnitureDataProvider`: furniture persistence provider boundary.
- `ICatalogDataProvider`: catalog persistence provider boundary. - `ICatalogDataProvider`: catalog persistence provider boundary.
- `IFurnitureRepository`: furniture DAL composition boundary.
- `ICatalogRepository`: catalog DAL composition boundary.
- `ICatalogProjectionContractAdapter`: building-block catalog to DAL contract adapter boundary.
- `IFurnitureDalGrpcContractAdapter`: gRPC translation boundary for DAL contracts.
## Rules ## Rules
- Providers encapsulate datastore-specific access. - Providers encapsulate datastore-specific access.
- Providers do not contain orchestration concerns. - Providers do not contain orchestration concerns.
- Provider contracts are consumed by DAL repositories. - Provider contracts are consumed by DAL repositories.
- Repository and adapter boundaries expose contracts only in this stage.

View File

@ -0,0 +1,25 @@
using BuildingBlock.Catalog.Contracts.Products;
using BuildingBlock.Catalog.Contracts.Responses;
using Furniture.DAL.Contracts;
namespace Furniture.DAL.Adapters;
/// <summary>
/// Defines adapter boundary between catalog building block contracts and furniture dal contracts.
/// </summary>
public interface ICatalogProjectionContractAdapter
{
/// <summary>
/// Maps catalog capability product contract into dal lookup request.
/// </summary>
/// <param name="contract">Catalog product contract.</param>
/// <returns>Catalog product lookup request contract.</returns>
CatalogProductLookupRequest ToDalRequest(ProductContract contract);
/// <summary>
/// Maps dal catalog projection record into catalog capability response contract.
/// </summary>
/// <param name="record">Catalog product projection record.</param>
/// <returns>Catalog product response contract.</returns>
ProductContractResponse ToCatalogResponse(CatalogProductProjectionRecord record);
}

View File

@ -0,0 +1,38 @@
using Furniture.DAL.Contracts;
using Furniture.DAL.Grpc;
namespace Furniture.DAL.Adapters;
/// <summary>
/// Defines adapter boundary for furniture dal gRPC contract translation.
/// </summary>
public interface IFurnitureDalGrpcContractAdapter
{
/// <summary>
/// Maps transport-neutral availability lookup request to gRPC contract shape.
/// </summary>
/// <param name="request">Furniture availability lookup request contract.</param>
/// <returns>gRPC availability request contract.</returns>
FurnitureAvailabilityDalGrpcContract ToGrpcAvailabilityRequest(FurnitureAvailabilityLookupRequest request);
/// <summary>
/// Maps gRPC availability contract shape to transport-neutral request.
/// </summary>
/// <param name="contract">gRPC availability request contract.</param>
/// <returns>Furniture availability lookup request contract.</returns>
FurnitureAvailabilityLookupRequest FromGrpcAvailabilityRequest(FurnitureAvailabilityDalGrpcContract contract);
/// <summary>
/// Maps transport-neutral catalog lookup request to gRPC contract shape.
/// </summary>
/// <param name="request">Catalog product lookup request contract.</param>
/// <returns>gRPC catalog request contract.</returns>
CatalogProductDalGrpcContract ToGrpcCatalogRequest(CatalogProductLookupRequest request);
/// <summary>
/// Maps gRPC catalog contract shape to transport-neutral request.
/// </summary>
/// <param name="contract">gRPC catalog request contract.</param>
/// <returns>Catalog product lookup request contract.</returns>
CatalogProductLookupRequest FromGrpcCatalogRequest(CatalogProductDalGrpcContract contract);
}

View File

@ -1,3 +1,5 @@
using Furniture.DAL.Contracts;
namespace Furniture.DAL.Caching; namespace Furniture.DAL.Caching;
/// <summary> /// <summary>
@ -5,4 +7,17 @@ namespace Furniture.DAL.Caching;
/// </summary> /// </summary>
public interface ICacheInvalidationPolicy public interface ICacheInvalidationPolicy
{ {
/// <summary>
/// Builds cache key for a furniture availability request.
/// </summary>
/// <param name="request">Furniture availability lookup request contract.</param>
/// <returns>Cache key string.</returns>
string BuildAvailabilityKey(FurnitureAvailabilityLookupRequest request);
/// <summary>
/// Resolves cache invalidation keys for a mutation request boundary.
/// </summary>
/// <param name="request">Cache invalidation request contract.</param>
/// <returns>Cache keys to invalidate.</returns>
IReadOnlyList<string> ResolveInvalidationKeys(FurnitureCacheInvalidationRequest request);
} }

View File

@ -0,0 +1,8 @@
namespace Furniture.DAL.Contracts;
/// <summary>
/// Request contract for catalog product lookup.
/// </summary>
/// <param name="Envelope">Contract envelope metadata.</param>
/// <param name="ProductId">Catalog product identifier.</param>
public sealed record CatalogProductLookupRequest(FurnitureDalContractEnvelope Envelope, string ProductId);

View File

@ -0,0 +1,12 @@
namespace Furniture.DAL.Contracts;
/// <summary>
/// Response contract representing catalog product projection data.
/// </summary>
/// <param name="Envelope">Contract envelope metadata.</param>
/// <param name="ProductId">Catalog product identifier.</param>
/// <param name="DisplayName">Catalog display name.</param>
public sealed record CatalogProductProjectionRecord(
FurnitureDalContractEnvelope Envelope,
string ProductId,
string DisplayName);

View File

@ -0,0 +1,8 @@
namespace Furniture.DAL.Contracts;
/// <summary>
/// Request contract for furniture availability lookup.
/// </summary>
/// <param name="Envelope">Contract envelope metadata.</param>
/// <param name="FurnitureId">Furniture aggregate identifier.</param>
public sealed record FurnitureAvailabilityLookupRequest(FurnitureDalContractEnvelope Envelope, string FurnitureId);

View File

@ -0,0 +1,12 @@
namespace Furniture.DAL.Contracts;
/// <summary>
/// Response contract representing persisted furniture availability data.
/// </summary>
/// <param name="Envelope">Contract envelope metadata.</param>
/// <param name="FurnitureId">Furniture aggregate identifier.</param>
/// <param name="QuantityAvailable">Current quantity available.</param>
public sealed record FurnitureAvailabilityRecord(
FurnitureDalContractEnvelope Envelope,
string FurnitureId,
int QuantityAvailable);

View File

@ -0,0 +1,12 @@
namespace Furniture.DAL.Contracts;
/// <summary>
/// Request contract for cache invalidation boundaries.
/// </summary>
/// <param name="Envelope">Contract envelope metadata.</param>
/// <param name="FurnitureId">Furniture aggregate identifier that changed.</param>
/// <param name="Reason">Invalidation reason code.</param>
public sealed record FurnitureCacheInvalidationRequest(
FurnitureDalContractEnvelope Envelope,
string FurnitureId,
string Reason);

View File

@ -0,0 +1,8 @@
namespace Furniture.DAL.Contracts;
/// <summary>
/// Defines transport-neutral envelope metadata for furniture dal contract messages.
/// </summary>
/// <param name="ContractVersion">Contract schema version.</param>
/// <param name="CorrelationId">Correlation identifier for cross-layer tracing.</param>
public sealed record FurnitureDalContractEnvelope(string ContractVersion, string CorrelationId);

View File

@ -0,0 +1,15 @@
using Core.Blueprint.Common.Contracts;
namespace Furniture.DAL.Contracts;
/// <summary>
/// Defines package descriptor metadata for furniture dal contracts.
/// </summary>
public sealed class FurnitureDalPackageContract : IBlueprintPackageContract
{
/// <inheritdoc />
public BlueprintPackageDescriptor Descriptor { get; } = new(
"Furniture.DAL.Contracts",
PackageVersionPolicy.Minor,
["Core.Blueprint.Common", "BuildingBlock.Catalog.Contracts"]);
}

View File

@ -4,4 +4,8 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\blueprint-platform\src\Core.Blueprint.Common\Core.Blueprint.Common.csproj" />
<ProjectReference Include="..\..\..\building-block-catalog\src\BuildingBlock.Catalog.Contracts\BuildingBlock.Catalog.Contracts.csproj" />
</ItemGroup>
</Project> </Project>

View File

@ -0,0 +1,7 @@
namespace Furniture.DAL.Grpc;
/// <summary>
/// Defines minimal gRPC contract shape for catalog projection dal translation.
/// </summary>
/// <param name="ProductId">Catalog product identifier.</param>
public sealed record CatalogProductDalGrpcContract(string ProductId);

View File

@ -0,0 +1,7 @@
namespace Furniture.DAL.Grpc;
/// <summary>
/// Defines minimal gRPC contract shape for furniture availability dal translation.
/// </summary>
/// <param name="FurnitureId">Furniture aggregate identifier.</param>
public sealed record FurnitureAvailabilityDalGrpcContract(string FurnitureId);

View File

@ -1,3 +1,5 @@
using Furniture.DAL.Contracts;
namespace Furniture.DAL.Providers; namespace Furniture.DAL.Providers;
/// <summary> /// <summary>
@ -5,4 +7,13 @@ namespace Furniture.DAL.Providers;
/// </summary> /// </summary>
public interface ICatalogDataProvider public interface ICatalogDataProvider
{ {
/// <summary>
/// Reads catalog projection data for furniture scope.
/// </summary>
/// <param name="request">Catalog product lookup request contract.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Catalog product projection when found; otherwise null.</returns>
Task<CatalogProductProjectionRecord?> ReadProductAsync(
CatalogProductLookupRequest request,
CancellationToken cancellationToken = default);
} }

View File

@ -1,3 +1,5 @@
using Furniture.DAL.Contracts;
namespace Furniture.DAL.Providers; namespace Furniture.DAL.Providers;
/// <summary> /// <summary>
@ -5,4 +7,13 @@ namespace Furniture.DAL.Providers;
/// </summary> /// </summary>
public interface IFurnitureDataProvider public interface IFurnitureDataProvider
{ {
/// <summary>
/// Reads furniture availability persistence data.
/// </summary>
/// <param name="request">Furniture availability lookup request contract.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Furniture availability record when found; otherwise null.</returns>
Task<FurnitureAvailabilityRecord?> ReadAvailabilityAsync(
FurnitureAvailabilityLookupRequest request,
CancellationToken cancellationToken = default);
} }

View File

@ -1,3 +1,5 @@
using Furniture.DAL.Contracts;
namespace Furniture.DAL.Repositories; namespace Furniture.DAL.Repositories;
/// <summary> /// <summary>
@ -5,4 +7,13 @@ namespace Furniture.DAL.Repositories;
/// </summary> /// </summary>
public interface ICatalogRepository public interface ICatalogRepository
{ {
/// <summary>
/// Reads catalog product projection for furniture scope.
/// </summary>
/// <param name="request">Catalog product lookup request contract.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Catalog product projection when found; otherwise null.</returns>
Task<CatalogProductProjectionRecord?> ReadProductAsync(
CatalogProductLookupRequest request,
CancellationToken cancellationToken = default);
} }

View File

@ -1,3 +1,5 @@
using Furniture.DAL.Contracts;
namespace Furniture.DAL.Repositories; namespace Furniture.DAL.Repositories;
/// <summary> /// <summary>
@ -5,4 +7,13 @@ namespace Furniture.DAL.Repositories;
/// </summary> /// </summary>
public interface IFurnitureRepository public interface IFurnitureRepository
{ {
/// <summary>
/// Reads availability for a furniture aggregate.
/// </summary>
/// <param name="request">Furniture availability lookup request contract.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Furniture availability record when found; otherwise null.</returns>
Task<FurnitureAvailabilityRecord?> ReadAvailabilityAsync(
FurnitureAvailabilityLookupRequest request,
CancellationToken cancellationToken = default);
} }

View File

@ -1,6 +1,8 @@
using Furniture.DAL.Caching; using Furniture.DAL.Caching;
using Furniture.DAL.Adapters;
using Furniture.DAL.Providers; using Furniture.DAL.Providers;
using Furniture.DAL.Repositories; using Furniture.DAL.Repositories;
using System.Reflection;
namespace Furniture.DAL.UnitTests; namespace Furniture.DAL.UnitTests;
@ -20,4 +22,35 @@ public class BoundaryShapeTests
Assert.True(typeof(ICatalogRepository).IsInterface); Assert.True(typeof(ICatalogRepository).IsInterface);
Assert.True(typeof(ICacheInvalidationPolicy).IsInterface); Assert.True(typeof(ICacheInvalidationPolicy).IsInterface);
} }
[Fact]
public void RepositoryBoundaries_WhenReflected_ExposeReadOnlyContracts()
{
Assert.Equal("ReadAvailabilityAsync", typeof(IFurnitureRepository).GetMethods().Single().Name);
Assert.Equal("ReadProductAsync", typeof(ICatalogRepository).GetMethods().Single().Name);
}
[Fact]
public void ProviderBoundaries_WhenReflected_ExposeReadOnlyContracts()
{
Assert.Equal("ReadAvailabilityAsync", typeof(IFurnitureDataProvider).GetMethods().Single().Name);
Assert.Equal("ReadProductAsync", typeof(ICatalogDataProvider).GetMethods().Single().Name);
}
[Fact]
public void AdapterBoundaries_WhenReflected_AreInterfaces()
{
Assert.True(typeof(IFurnitureDalGrpcContractAdapter).IsInterface);
Assert.True(typeof(ICatalogProjectionContractAdapter).IsInterface);
var grpcMethodNames = typeof(IFurnitureDalGrpcContractAdapter)
.GetMethods(BindingFlags.Public | BindingFlags.Instance)
.Select(method => method.Name)
.OrderBy(name => name)
.ToArray();
Assert.Equal(
["FromGrpcAvailabilityRequest", "FromGrpcCatalogRequest", "ToGrpcAvailabilityRequest", "ToGrpcCatalogRequest"],
grpcMethodNames);
}
} }

View File

@ -0,0 +1,41 @@
using Core.Blueprint.Common.Contracts;
using Furniture.DAL.Contracts;
namespace Furniture.DAL.UnitTests;
public class ContractShapeTests
{
[Fact]
public void FurnitureAvailabilityLookupRequest_WhenCreated_StoresTransportNeutralData()
{
var envelope = new FurnitureDalContractEnvelope("1.0.0", "corr-123");
var request = new FurnitureAvailabilityLookupRequest(envelope, "FUR-001");
Assert.Equal("1.0.0", request.Envelope.ContractVersion);
Assert.Equal("corr-123", request.Envelope.CorrelationId);
Assert.Equal("FUR-001", request.FurnitureId);
}
[Fact]
public void CatalogProductProjectionRecord_WhenCreated_StoresTransportNeutralData()
{
var envelope = new FurnitureDalContractEnvelope("1.0.0", "corr-123");
var record = new CatalogProductProjectionRecord(envelope, "PRD-001", "Chair");
Assert.Equal("1.0.0", record.Envelope.ContractVersion);
Assert.Equal("corr-123", record.Envelope.CorrelationId);
Assert.Equal("PRD-001", record.ProductId);
Assert.Equal("Chair", record.DisplayName);
}
[Fact]
public void FurnitureDalPackageContract_WhenCreated_UsesBlueprintDescriptorContract()
{
IBlueprintPackageContract contract = new FurnitureDalPackageContract();
Assert.Equal("Furniture.DAL.Contracts", contract.Descriptor.PackageId);
Assert.Equal(PackageVersionPolicy.Minor, contract.Descriptor.VersionPolicy);
Assert.Contains("Core.Blueprint.Common", contract.Descriptor.DependencyPackageIds);
Assert.Contains("BuildingBlock.Catalog.Contracts", contract.Descriptor.DependencyPackageIds);
}
}