From 509380c43e07ca2ada0b06c114e38986c1ce7b7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Ren=C3=A9=20White=20Enciso?= Date: Sun, 22 Feb 2026 18:27:50 -0600 Subject: [PATCH 1/3] feat(furniture-dal): add runtime providers and repository wiring --- .../CatalogProjectionContractAdapter.cs | 35 +++++++++ .../FurnitureDalGrpcContractAdapter.cs | 40 ++++++++++ .../FurnitureCacheInvalidationPolicy.cs | 24 ++++++ .../Contracts/DalDependencyHealthStatus.cs | 12 +++ ...FurnitureDalServiceCollectionExtensions.cs | 40 ++++++++++ src/Furniture.DAL/Furniture.DAL.csproj | 1 + .../Health/DalDependencyHealthCheck.cs | 27 +++++++ .../Health/IDalDependencyHealthCheck.cs | 16 ++++ .../InMemory/InMemoryCatalogDataProvider.cs | 35 +++++++++ .../InMemory/InMemoryFurnitureDataProvider.cs | 35 +++++++++ .../Repositories/CatalogRepository.cs | 18 +++++ .../Repositories/FurnitureRepository.cs | 38 ++++++++++ .../Furniture.DAL.UnitTests.csproj | 1 + .../RuntimeWiringTests.cs | 76 +++++++++++++++++++ 14 files changed, 398 insertions(+) create mode 100644 src/Furniture.DAL/Adapters/CatalogProjectionContractAdapter.cs create mode 100644 src/Furniture.DAL/Adapters/FurnitureDalGrpcContractAdapter.cs create mode 100644 src/Furniture.DAL/Caching/FurnitureCacheInvalidationPolicy.cs create mode 100644 src/Furniture.DAL/Contracts/DalDependencyHealthStatus.cs create mode 100644 src/Furniture.DAL/DependencyInjection/FurnitureDalServiceCollectionExtensions.cs create mode 100644 src/Furniture.DAL/Health/DalDependencyHealthCheck.cs create mode 100644 src/Furniture.DAL/Health/IDalDependencyHealthCheck.cs create mode 100644 src/Furniture.DAL/Providers/InMemory/InMemoryCatalogDataProvider.cs create mode 100644 src/Furniture.DAL/Providers/InMemory/InMemoryFurnitureDataProvider.cs create mode 100644 src/Furniture.DAL/Repositories/CatalogRepository.cs create mode 100644 src/Furniture.DAL/Repositories/FurnitureRepository.cs create mode 100644 tests/Furniture.DAL.UnitTests/RuntimeWiringTests.cs diff --git a/src/Furniture.DAL/Adapters/CatalogProjectionContractAdapter.cs b/src/Furniture.DAL/Adapters/CatalogProjectionContractAdapter.cs new file mode 100644 index 0000000..7edc830 --- /dev/null +++ b/src/Furniture.DAL/Adapters/CatalogProjectionContractAdapter.cs @@ -0,0 +1,35 @@ +using BuildingBlock.Catalog.Contracts.Conventions; +using BuildingBlock.Catalog.Contracts.Products; +using BuildingBlock.Catalog.Contracts.Responses; +using Furniture.DAL.Contracts; + +namespace Furniture.DAL.Adapters; + +/// +/// Default adapter implementation for catalog building block and furniture DAL contracts. +/// +public sealed class CatalogProjectionContractAdapter : ICatalogProjectionContractAdapter +{ + /// + public CatalogProductLookupRequest ToDalRequest(ProductContract contract) + { + var envelope = new FurnitureDalContractEnvelope( + contract.Envelope.ContractVersion, + contract.Envelope.CorrelationId); + + return new CatalogProductLookupRequest(envelope, contract.ProductId); + } + + /// + public ProductContractResponse ToCatalogResponse(CatalogProductProjectionRecord record) + { + var envelope = new CatalogContractEnvelope( + record.Envelope.ContractVersion, + record.Envelope.CorrelationId); + + return new ProductContractResponse( + envelope, + record.ProductId, + record.DisplayName); + } +} diff --git a/src/Furniture.DAL/Adapters/FurnitureDalGrpcContractAdapter.cs b/src/Furniture.DAL/Adapters/FurnitureDalGrpcContractAdapter.cs new file mode 100644 index 0000000..8e9f627 --- /dev/null +++ b/src/Furniture.DAL/Adapters/FurnitureDalGrpcContractAdapter.cs @@ -0,0 +1,40 @@ +using Core.Blueprint.Common.Runtime; +using Furniture.DAL.Contracts; +using Furniture.DAL.Grpc; + +namespace Furniture.DAL.Adapters; + +/// +/// Default adapter implementation for furniture DAL gRPC contract translation. +/// +public sealed class FurnitureDalGrpcContractAdapter(IBlueprintSystemClock clock) : IFurnitureDalGrpcContractAdapter +{ + /// + public FurnitureAvailabilityDalGrpcContract ToGrpcAvailabilityRequest(FurnitureAvailabilityLookupRequest request) + { + return new FurnitureAvailabilityDalGrpcContract(request.FurnitureId); + } + + /// + public FurnitureAvailabilityLookupRequest FromGrpcAvailabilityRequest(FurnitureAvailabilityDalGrpcContract contract) + { + return new FurnitureAvailabilityLookupRequest(CreateEnvelope(), contract.FurnitureId); + } + + /// + public CatalogProductDalGrpcContract ToGrpcCatalogRequest(CatalogProductLookupRequest request) + { + return new CatalogProductDalGrpcContract(request.ProductId); + } + + /// + public CatalogProductLookupRequest FromGrpcCatalogRequest(CatalogProductDalGrpcContract contract) + { + return new CatalogProductLookupRequest(CreateEnvelope(), contract.ProductId); + } + + private FurnitureDalContractEnvelope CreateEnvelope() + { + return new FurnitureDalContractEnvelope("1.0.0", $"corr-{clock.UtcNow:yyyyMMddHHmmssfff}"); + } +} diff --git a/src/Furniture.DAL/Caching/FurnitureCacheInvalidationPolicy.cs b/src/Furniture.DAL/Caching/FurnitureCacheInvalidationPolicy.cs new file mode 100644 index 0000000..8dc341d --- /dev/null +++ b/src/Furniture.DAL/Caching/FurnitureCacheInvalidationPolicy.cs @@ -0,0 +1,24 @@ +using Furniture.DAL.Contracts; + +namespace Furniture.DAL.Caching; + +/// +/// Default cache invalidation policy for furniture DAL runtime. +/// +public sealed class FurnitureCacheInvalidationPolicy : ICacheInvalidationPolicy +{ + /// + public string BuildAvailabilityKey(FurnitureAvailabilityLookupRequest request) + { + return $"furniture:availability:{request.FurnitureId}"; + } + + /// + public IReadOnlyList ResolveInvalidationKeys(FurnitureCacheInvalidationRequest request) + { + return + [ + $"furniture:availability:{request.FurnitureId}" + ]; + } +} diff --git a/src/Furniture.DAL/Contracts/DalDependencyHealthStatus.cs b/src/Furniture.DAL/Contracts/DalDependencyHealthStatus.cs new file mode 100644 index 0000000..9083939 --- /dev/null +++ b/src/Furniture.DAL/Contracts/DalDependencyHealthStatus.cs @@ -0,0 +1,12 @@ +namespace Furniture.DAL.Contracts; + +/// +/// Response contract for furniture DAL dependency health evaluation. +/// +/// Contract envelope metadata. +/// Indicates whether all runtime dependencies are healthy. +/// Dependency names included in health evaluation. +public sealed record DalDependencyHealthStatus( + FurnitureDalContractEnvelope Envelope, + bool IsHealthy, + IReadOnlyList DependencyNames); diff --git a/src/Furniture.DAL/DependencyInjection/FurnitureDalServiceCollectionExtensions.cs b/src/Furniture.DAL/DependencyInjection/FurnitureDalServiceCollectionExtensions.cs new file mode 100644 index 0000000..2c648e1 --- /dev/null +++ b/src/Furniture.DAL/DependencyInjection/FurnitureDalServiceCollectionExtensions.cs @@ -0,0 +1,40 @@ +using Core.Blueprint.Common.DependencyInjection; +using Furniture.DAL.Adapters; +using Furniture.DAL.Caching; +using Furniture.DAL.Health; +using Furniture.DAL.Providers; +using Furniture.DAL.Providers.InMemory; +using Furniture.DAL.Repositories; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Furniture.DAL.DependencyInjection; + +/// +/// Registers furniture DAL runtime provider, repository, and adapter implementations. +/// +public static class FurnitureDalServiceCollectionExtensions +{ + /// + /// Adds furniture DAL runtime implementations aligned with blueprint runtime core. + /// + /// Service collection. + /// Service collection for fluent chaining. + public static IServiceCollection AddFurnitureDalRuntime(this IServiceCollection services) + { + services.AddBlueprintRuntimeCore(); + + services.TryAddSingleton(); + services.TryAddSingleton(); + + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + + return services; + } +} diff --git a/src/Furniture.DAL/Furniture.DAL.csproj b/src/Furniture.DAL/Furniture.DAL.csproj index d824f85..ba51331 100644 --- a/src/Furniture.DAL/Furniture.DAL.csproj +++ b/src/Furniture.DAL/Furniture.DAL.csproj @@ -5,6 +5,7 @@ enable + diff --git a/src/Furniture.DAL/Health/DalDependencyHealthCheck.cs b/src/Furniture.DAL/Health/DalDependencyHealthCheck.cs new file mode 100644 index 0000000..0b5bc8f --- /dev/null +++ b/src/Furniture.DAL/Health/DalDependencyHealthCheck.cs @@ -0,0 +1,27 @@ +using Core.Blueprint.Common.Runtime; +using Furniture.DAL.Contracts; + +namespace Furniture.DAL.Health; + +/// +/// Default DAL dependency health check implementation. +/// +public sealed class DalDependencyHealthCheck(IBlueprintSystemClock clock) : IDalDependencyHealthCheck +{ + /// + public Task CheckAsync(CancellationToken cancellationToken = default) + { + var envelope = new FurnitureDalContractEnvelope("1.0.0", $"corr-{clock.UtcNow:yyyyMMddHHmmssfff}"); + IReadOnlyList dependencyNames = + [ + "IFurnitureDataProvider", + "ICatalogDataProvider", + "IFurnitureRepository", + "ICatalogRepository", + "ICacheInvalidationPolicy" + ]; + + var status = new DalDependencyHealthStatus(envelope, true, dependencyNames); + return Task.FromResult(status); + } +} diff --git a/src/Furniture.DAL/Health/IDalDependencyHealthCheck.cs b/src/Furniture.DAL/Health/IDalDependencyHealthCheck.cs new file mode 100644 index 0000000..26eec93 --- /dev/null +++ b/src/Furniture.DAL/Health/IDalDependencyHealthCheck.cs @@ -0,0 +1,16 @@ +using Furniture.DAL.Contracts; + +namespace Furniture.DAL.Health; + +/// +/// Defines furniture DAL dependency health check boundary. +/// +public interface IDalDependencyHealthCheck +{ + /// + /// Evaluates runtime dependency health for DAL providers and repositories. + /// + /// Cancellation token. + /// Dependency health status contract. + Task CheckAsync(CancellationToken cancellationToken = default); +} diff --git a/src/Furniture.DAL/Providers/InMemory/InMemoryCatalogDataProvider.cs b/src/Furniture.DAL/Providers/InMemory/InMemoryCatalogDataProvider.cs new file mode 100644 index 0000000..134951b --- /dev/null +++ b/src/Furniture.DAL/Providers/InMemory/InMemoryCatalogDataProvider.cs @@ -0,0 +1,35 @@ +using Furniture.DAL.Contracts; + +namespace Furniture.DAL.Providers.InMemory; + +/// +/// In-memory provider implementation for catalog persistence reads. +/// +public sealed class InMemoryCatalogDataProvider : ICatalogDataProvider +{ + private static readonly IReadOnlyDictionary DisplayNameByProductId = + new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["PRD-001"] = "Contoso Lounge Chair", + ["PRD-002"] = "Fabrikam Office Desk", + ["PRD-003"] = "Northwind Shelf" + }; + + /// + public Task ReadProductAsync( + CatalogProductLookupRequest request, + CancellationToken cancellationToken = default) + { + if (!DisplayNameByProductId.TryGetValue(request.ProductId, out var displayName)) + { + return Task.FromResult(null); + } + + var record = new CatalogProductProjectionRecord( + request.Envelope, + request.ProductId, + displayName); + + return Task.FromResult(record); + } +} diff --git a/src/Furniture.DAL/Providers/InMemory/InMemoryFurnitureDataProvider.cs b/src/Furniture.DAL/Providers/InMemory/InMemoryFurnitureDataProvider.cs new file mode 100644 index 0000000..08cd3ac --- /dev/null +++ b/src/Furniture.DAL/Providers/InMemory/InMemoryFurnitureDataProvider.cs @@ -0,0 +1,35 @@ +using Furniture.DAL.Contracts; + +namespace Furniture.DAL.Providers.InMemory; + +/// +/// In-memory provider implementation for furniture persistence reads. +/// +public sealed class InMemoryFurnitureDataProvider : IFurnitureDataProvider +{ + private static readonly IReadOnlyDictionary AvailabilityByFurnitureId = + new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["FUR-001"] = 8, + ["FUR-002"] = 2, + ["FUR-003"] = 0 + }; + + /// + public Task ReadAvailabilityAsync( + FurnitureAvailabilityLookupRequest request, + CancellationToken cancellationToken = default) + { + if (!AvailabilityByFurnitureId.TryGetValue(request.FurnitureId, out var quantityAvailable)) + { + return Task.FromResult(null); + } + + var record = new FurnitureAvailabilityRecord( + request.Envelope, + request.FurnitureId, + quantityAvailable); + + return Task.FromResult(record); + } +} diff --git a/src/Furniture.DAL/Repositories/CatalogRepository.cs b/src/Furniture.DAL/Repositories/CatalogRepository.cs new file mode 100644 index 0000000..d8ee92a --- /dev/null +++ b/src/Furniture.DAL/Repositories/CatalogRepository.cs @@ -0,0 +1,18 @@ +using Furniture.DAL.Contracts; +using Furniture.DAL.Providers; + +namespace Furniture.DAL.Repositories; + +/// +/// Default catalog repository implementation composed from DAL providers. +/// +public sealed class CatalogRepository(ICatalogDataProvider catalogDataProvider) : ICatalogRepository +{ + /// + public Task ReadProductAsync( + CatalogProductLookupRequest request, + CancellationToken cancellationToken = default) + { + return catalogDataProvider.ReadProductAsync(request, cancellationToken); + } +} diff --git a/src/Furniture.DAL/Repositories/FurnitureRepository.cs b/src/Furniture.DAL/Repositories/FurnitureRepository.cs new file mode 100644 index 0000000..b7e903b --- /dev/null +++ b/src/Furniture.DAL/Repositories/FurnitureRepository.cs @@ -0,0 +1,38 @@ +using System.Collections.Concurrent; +using Furniture.DAL.Caching; +using Furniture.DAL.Contracts; +using Furniture.DAL.Providers; + +namespace Furniture.DAL.Repositories; + +/// +/// Default furniture repository implementation composed from DAL providers. +/// +public sealed class FurnitureRepository( + IFurnitureDataProvider furnitureDataProvider, + ICacheInvalidationPolicy cacheInvalidationPolicy) : IFurnitureRepository +{ + private readonly ConcurrentDictionary availabilityCache = new( + StringComparer.OrdinalIgnoreCase); + + /// + public async Task ReadAvailabilityAsync( + FurnitureAvailabilityLookupRequest request, + CancellationToken cancellationToken = default) + { + var cacheKey = cacheInvalidationPolicy.BuildAvailabilityKey(request); + if (availabilityCache.TryGetValue(cacheKey, out var cachedRecord)) + { + return cachedRecord with { Envelope = request.Envelope }; + } + + var providerRecord = await furnitureDataProvider.ReadAvailabilityAsync(request, cancellationToken); + if (providerRecord is null) + { + return null; + } + + availabilityCache[cacheKey] = providerRecord; + return providerRecord; + } +} diff --git a/tests/Furniture.DAL.UnitTests/Furniture.DAL.UnitTests.csproj b/tests/Furniture.DAL.UnitTests/Furniture.DAL.UnitTests.csproj index 07412cf..0aefa51 100644 --- a/tests/Furniture.DAL.UnitTests/Furniture.DAL.UnitTests.csproj +++ b/tests/Furniture.DAL.UnitTests/Furniture.DAL.UnitTests.csproj @@ -7,6 +7,7 @@ + diff --git a/tests/Furniture.DAL.UnitTests/RuntimeWiringTests.cs b/tests/Furniture.DAL.UnitTests/RuntimeWiringTests.cs new file mode 100644 index 0000000..4e9edf4 --- /dev/null +++ b/tests/Furniture.DAL.UnitTests/RuntimeWiringTests.cs @@ -0,0 +1,76 @@ +using Furniture.DAL.Adapters; +using Furniture.DAL.Contracts; +using Furniture.DAL.DependencyInjection; +using Furniture.DAL.Health; +using Furniture.DAL.Repositories; +using Microsoft.Extensions.DependencyInjection; + +namespace Furniture.DAL.UnitTests; + +public class RuntimeWiringTests +{ + [Fact] + public async Task AddFurnitureDalRuntime_WhenResolved_WiresRepositoriesAndProviders() + { + var services = new ServiceCollection(); + services.AddFurnitureDalRuntime(); + + using var provider = services.BuildServiceProvider(); + var furnitureRepository = provider.GetRequiredService(); + var catalogRepository = provider.GetRequiredService(); + var envelope = new FurnitureDalContractEnvelope("1.0.0", "corr-123"); + + var availability = await furnitureRepository.ReadAvailabilityAsync( + new FurnitureAvailabilityLookupRequest(envelope, "FUR-001")); + var product = await catalogRepository.ReadProductAsync( + new CatalogProductLookupRequest(envelope, "PRD-001")); + + Assert.NotNull(availability); + Assert.Equal("FUR-001", availability.FurnitureId); + Assert.Equal(8, availability.QuantityAvailable); + + Assert.NotNull(product); + Assert.Equal("PRD-001", product.ProductId); + Assert.Equal("Contoso Lounge Chair", product.DisplayName); + } + + [Fact] + public void AddFurnitureDalRuntime_WhenResolved_WiresGrpcAndCatalogAdapters() + { + var services = new ServiceCollection(); + services.AddFurnitureDalRuntime(); + + using var provider = services.BuildServiceProvider(); + var grpcAdapter = provider.GetRequiredService(); + var projectionAdapter = provider.GetRequiredService(); + + var availabilityRequest = grpcAdapter.FromGrpcAvailabilityRequest( + new Furniture.DAL.Grpc.FurnitureAvailabilityDalGrpcContract("FUR-002")); + var catalogRequest = projectionAdapter.ToDalRequest( + new BuildingBlock.Catalog.Contracts.Products.ProductContract( + new BuildingBlock.Catalog.Contracts.Conventions.CatalogContractEnvelope("1.0.0", "corr-456"), + "PRD-002", + "Fabrikam Office Desk")); + + Assert.Equal("FUR-002", availabilityRequest.FurnitureId); + Assert.NotEmpty(availabilityRequest.Envelope.CorrelationId); + Assert.Equal("PRD-002", catalogRequest.ProductId); + Assert.Equal("corr-456", catalogRequest.Envelope.CorrelationId); + } + + [Fact] + public async Task AddFurnitureDalRuntime_WhenResolved_WiresDependencyHealthCheck() + { + var services = new ServiceCollection(); + services.AddFurnitureDalRuntime(); + + using var provider = services.BuildServiceProvider(); + var healthCheck = provider.GetRequiredService(); + + var status = await healthCheck.CheckAsync(); + + Assert.True(status.IsHealthy); + Assert.Contains("IFurnitureDataProvider", status.DependencyNames); + Assert.Contains("ICatalogRepository", status.DependencyNames); + } +} From 9455bc865a3b953c5e62d93155dfd7bc1d6a835f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Ren=C3=A9=20White=20Enciso?= Date: Tue, 24 Feb 2026 05:26:54 -0600 Subject: [PATCH 2/3] docs(furniture-dal): add domain boundary notes --- docs/architecture/dal-domain-alignment.md | 13 +++++++++++++ docs/migration/dal-port-alignment-map.md | 6 ++++++ docs/migration/technical-mapping-rules.md | 6 ++++++ 3 files changed, 25 insertions(+) create mode 100644 docs/architecture/dal-domain-alignment.md create mode 100644 docs/migration/dal-port-alignment-map.md create mode 100644 docs/migration/technical-mapping-rules.md diff --git a/docs/architecture/dal-domain-alignment.md b/docs/architecture/dal-domain-alignment.md new file mode 100644 index 0000000..1288d1c --- /dev/null +++ b/docs/architecture/dal-domain-alignment.md @@ -0,0 +1,13 @@ +# Furniture DAL Domain Alignment + +## Goal +Align DAL with furniture-domain abstractions while keeping DAL technical. + +## DAL Responsibilities +- Persistence and retrieval +- Technical data translation +- Provider/repository boundaries + +## Prohibited +- Domain decision ownership +- Service orchestration concerns diff --git a/docs/migration/dal-port-alignment-map.md b/docs/migration/dal-port-alignment-map.md new file mode 100644 index 0000000..d234c3d --- /dev/null +++ b/docs/migration/dal-port-alignment-map.md @@ -0,0 +1,6 @@ +# DAL Port Alignment Map + +## Alignment Areas +- DAL read/write ports map to domain contracts. +- Technical DTO translation remains in DAL adapters. +- Domain invariants are not reimplemented in DAL. diff --git a/docs/migration/technical-mapping-rules.md b/docs/migration/technical-mapping-rules.md new file mode 100644 index 0000000..f7cff99 --- /dev/null +++ b/docs/migration/technical-mapping-rules.md @@ -0,0 +1,6 @@ +# Technical Mapping Rules + +## Rules +- Mapping logic must remain technical and deterministic. +- No business branching in DAL mapping layer. +- Correlation and metadata pass-through remains unchanged. From 99d592322408baf06d5cb35c0043240fe6f37fd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Ren=C3=A9=20White=20Enciso?= Date: Wed, 25 Feb 2026 13:13:56 -0600 Subject: [PATCH 3/3] docs(furniture-dal): clarify dal to domain boundary rules --- docs/architecture/dal-domain-alignment.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/architecture/dal-domain-alignment.md b/docs/architecture/dal-domain-alignment.md index 1288d1c..0340c8a 100644 --- a/docs/architecture/dal-domain-alignment.md +++ b/docs/architecture/dal-domain-alignment.md @@ -7,6 +7,7 @@ Align DAL with furniture-domain abstractions while keeping DAL technical. - Persistence and retrieval - Technical data translation - Provider/repository boundaries +- Contract adaptation for service and domain boundaries without owning decisions ## Prohibited - Domain decision ownership