feat(furniture-dal): define dal contracts and adapter boundaries

This commit is contained in:
José René White Enciso 2026-02-22 04:39:56 -06:00
parent 94313c7378
commit 479416b907
22 changed files with 310 additions and 0 deletions

View File

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

View File

@ -5,8 +5,10 @@
- Repositories coordinate provider calls for aggregate persistence.
- Cache invalidation policy is owned by DAL.
- Service layer does not implement persistence or cache invalidation details.
- Cache invalidation boundaries are expressed through DAL contract interfaces.
## Policy
- Persistence writes must define cache invalidation triggers.
- 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.
- `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
- Providers encapsulate datastore-specific access.
- Providers do not contain orchestration concerns.
- 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;
/// <summary>
@ -5,4 +7,17 @@ namespace Furniture.DAL.Caching;
/// </summary>
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>
<Nullable>enable</Nullable>
</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>

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;
/// <summary>
@ -5,4 +7,13 @@ namespace Furniture.DAL.Providers;
/// </summary>
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;
/// <summary>
@ -5,4 +7,13 @@ namespace Furniture.DAL.Providers;
/// </summary>
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;
/// <summary>
@ -5,4 +7,13 @@ namespace Furniture.DAL.Repositories;
/// </summary>
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;
/// <summary>
@ -5,4 +7,13 @@ namespace Furniture.DAL.Repositories;
/// </summary>
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.Adapters;
using Furniture.DAL.Providers;
using Furniture.DAL.Repositories;
using System.Reflection;
namespace Furniture.DAL.UnitTests;
@ -20,4 +22,35 @@ public class BoundaryShapeTests
Assert.True(typeof(ICatalogRepository).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);
}
}