feat(furniture-dal): add runtime providers and repository wiring

This commit is contained in:
José René White Enciso 2026-02-22 18:27:50 -06:00
parent 20cbe1bb3b
commit 509380c43e
14 changed files with 398 additions and 0 deletions

View File

@ -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;
/// <summary>
/// Default adapter implementation for catalog building block and furniture DAL contracts.
/// </summary>
public sealed class CatalogProjectionContractAdapter : ICatalogProjectionContractAdapter
{
/// <inheritdoc />
public CatalogProductLookupRequest ToDalRequest(ProductContract contract)
{
var envelope = new FurnitureDalContractEnvelope(
contract.Envelope.ContractVersion,
contract.Envelope.CorrelationId);
return new CatalogProductLookupRequest(envelope, contract.ProductId);
}
/// <inheritdoc />
public ProductContractResponse ToCatalogResponse(CatalogProductProjectionRecord record)
{
var envelope = new CatalogContractEnvelope(
record.Envelope.ContractVersion,
record.Envelope.CorrelationId);
return new ProductContractResponse(
envelope,
record.ProductId,
record.DisplayName);
}
}

View File

@ -0,0 +1,40 @@
using Core.Blueprint.Common.Runtime;
using Furniture.DAL.Contracts;
using Furniture.DAL.Grpc;
namespace Furniture.DAL.Adapters;
/// <summary>
/// Default adapter implementation for furniture DAL gRPC contract translation.
/// </summary>
public sealed class FurnitureDalGrpcContractAdapter(IBlueprintSystemClock clock) : IFurnitureDalGrpcContractAdapter
{
/// <inheritdoc />
public FurnitureAvailabilityDalGrpcContract ToGrpcAvailabilityRequest(FurnitureAvailabilityLookupRequest request)
{
return new FurnitureAvailabilityDalGrpcContract(request.FurnitureId);
}
/// <inheritdoc />
public FurnitureAvailabilityLookupRequest FromGrpcAvailabilityRequest(FurnitureAvailabilityDalGrpcContract contract)
{
return new FurnitureAvailabilityLookupRequest(CreateEnvelope(), contract.FurnitureId);
}
/// <inheritdoc />
public CatalogProductDalGrpcContract ToGrpcCatalogRequest(CatalogProductLookupRequest request)
{
return new CatalogProductDalGrpcContract(request.ProductId);
}
/// <inheritdoc />
public CatalogProductLookupRequest FromGrpcCatalogRequest(CatalogProductDalGrpcContract contract)
{
return new CatalogProductLookupRequest(CreateEnvelope(), contract.ProductId);
}
private FurnitureDalContractEnvelope CreateEnvelope()
{
return new FurnitureDalContractEnvelope("1.0.0", $"corr-{clock.UtcNow:yyyyMMddHHmmssfff}");
}
}

View File

@ -0,0 +1,24 @@
using Furniture.DAL.Contracts;
namespace Furniture.DAL.Caching;
/// <summary>
/// Default cache invalidation policy for furniture DAL runtime.
/// </summary>
public sealed class FurnitureCacheInvalidationPolicy : ICacheInvalidationPolicy
{
/// <inheritdoc />
public string BuildAvailabilityKey(FurnitureAvailabilityLookupRequest request)
{
return $"furniture:availability:{request.FurnitureId}";
}
/// <inheritdoc />
public IReadOnlyList<string> ResolveInvalidationKeys(FurnitureCacheInvalidationRequest request)
{
return
[
$"furniture:availability:{request.FurnitureId}"
];
}
}

View File

@ -0,0 +1,12 @@
namespace Furniture.DAL.Contracts;
/// <summary>
/// Response contract for furniture DAL dependency health evaluation.
/// </summary>
/// <param name="Envelope">Contract envelope metadata.</param>
/// <param name="IsHealthy">Indicates whether all runtime dependencies are healthy.</param>
/// <param name="DependencyNames">Dependency names included in health evaluation.</param>
public sealed record DalDependencyHealthStatus(
FurnitureDalContractEnvelope Envelope,
bool IsHealthy,
IReadOnlyList<string> DependencyNames);

View File

@ -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;
/// <summary>
/// Registers furniture DAL runtime provider, repository, and adapter implementations.
/// </summary>
public static class FurnitureDalServiceCollectionExtensions
{
/// <summary>
/// Adds furniture DAL runtime implementations aligned with blueprint runtime core.
/// </summary>
/// <param name="services">Service collection.</param>
/// <returns>Service collection for fluent chaining.</returns>
public static IServiceCollection AddFurnitureDalRuntime(this IServiceCollection services)
{
services.AddBlueprintRuntimeCore();
services.TryAddSingleton<IFurnitureDataProvider, InMemoryFurnitureDataProvider>();
services.TryAddSingleton<ICatalogDataProvider, InMemoryCatalogDataProvider>();
services.TryAddSingleton<ICacheInvalidationPolicy, FurnitureCacheInvalidationPolicy>();
services.TryAddSingleton<IFurnitureRepository, FurnitureRepository>();
services.TryAddSingleton<ICatalogRepository, CatalogRepository>();
services.TryAddSingleton<ICatalogProjectionContractAdapter, CatalogProjectionContractAdapter>();
services.TryAddSingleton<IFurnitureDalGrpcContractAdapter, FurnitureDalGrpcContractAdapter>();
services.TryAddSingleton<IDalDependencyHealthCheck, DalDependencyHealthCheck>();
return services;
}
}

View File

@ -5,6 +5,7 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
<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>

View File

@ -0,0 +1,27 @@
using Core.Blueprint.Common.Runtime;
using Furniture.DAL.Contracts;
namespace Furniture.DAL.Health;
/// <summary>
/// Default DAL dependency health check implementation.
/// </summary>
public sealed class DalDependencyHealthCheck(IBlueprintSystemClock clock) : IDalDependencyHealthCheck
{
/// <inheritdoc />
public Task<DalDependencyHealthStatus> CheckAsync(CancellationToken cancellationToken = default)
{
var envelope = new FurnitureDalContractEnvelope("1.0.0", $"corr-{clock.UtcNow:yyyyMMddHHmmssfff}");
IReadOnlyList<string> dependencyNames =
[
"IFurnitureDataProvider",
"ICatalogDataProvider",
"IFurnitureRepository",
"ICatalogRepository",
"ICacheInvalidationPolicy"
];
var status = new DalDependencyHealthStatus(envelope, true, dependencyNames);
return Task.FromResult(status);
}
}

View File

@ -0,0 +1,16 @@
using Furniture.DAL.Contracts;
namespace Furniture.DAL.Health;
/// <summary>
/// Defines furniture DAL dependency health check boundary.
/// </summary>
public interface IDalDependencyHealthCheck
{
/// <summary>
/// Evaluates runtime dependency health for DAL providers and repositories.
/// </summary>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Dependency health status contract.</returns>
Task<DalDependencyHealthStatus> CheckAsync(CancellationToken cancellationToken = default);
}

View File

@ -0,0 +1,35 @@
using Furniture.DAL.Contracts;
namespace Furniture.DAL.Providers.InMemory;
/// <summary>
/// In-memory provider implementation for catalog persistence reads.
/// </summary>
public sealed class InMemoryCatalogDataProvider : ICatalogDataProvider
{
private static readonly IReadOnlyDictionary<string, string> DisplayNameByProductId =
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
["PRD-001"] = "Contoso Lounge Chair",
["PRD-002"] = "Fabrikam Office Desk",
["PRD-003"] = "Northwind Shelf"
};
/// <inheritdoc />
public Task<CatalogProductProjectionRecord?> ReadProductAsync(
CatalogProductLookupRequest request,
CancellationToken cancellationToken = default)
{
if (!DisplayNameByProductId.TryGetValue(request.ProductId, out var displayName))
{
return Task.FromResult<CatalogProductProjectionRecord?>(null);
}
var record = new CatalogProductProjectionRecord(
request.Envelope,
request.ProductId,
displayName);
return Task.FromResult<CatalogProductProjectionRecord?>(record);
}
}

View File

@ -0,0 +1,35 @@
using Furniture.DAL.Contracts;
namespace Furniture.DAL.Providers.InMemory;
/// <summary>
/// In-memory provider implementation for furniture persistence reads.
/// </summary>
public sealed class InMemoryFurnitureDataProvider : IFurnitureDataProvider
{
private static readonly IReadOnlyDictionary<string, int> AvailabilityByFurnitureId =
new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase)
{
["FUR-001"] = 8,
["FUR-002"] = 2,
["FUR-003"] = 0
};
/// <inheritdoc />
public Task<FurnitureAvailabilityRecord?> ReadAvailabilityAsync(
FurnitureAvailabilityLookupRequest request,
CancellationToken cancellationToken = default)
{
if (!AvailabilityByFurnitureId.TryGetValue(request.FurnitureId, out var quantityAvailable))
{
return Task.FromResult<FurnitureAvailabilityRecord?>(null);
}
var record = new FurnitureAvailabilityRecord(
request.Envelope,
request.FurnitureId,
quantityAvailable);
return Task.FromResult<FurnitureAvailabilityRecord?>(record);
}
}

View File

@ -0,0 +1,18 @@
using Furniture.DAL.Contracts;
using Furniture.DAL.Providers;
namespace Furniture.DAL.Repositories;
/// <summary>
/// Default catalog repository implementation composed from DAL providers.
/// </summary>
public sealed class CatalogRepository(ICatalogDataProvider catalogDataProvider) : ICatalogRepository
{
/// <inheritdoc />
public Task<CatalogProductProjectionRecord?> ReadProductAsync(
CatalogProductLookupRequest request,
CancellationToken cancellationToken = default)
{
return catalogDataProvider.ReadProductAsync(request, cancellationToken);
}
}

View File

@ -0,0 +1,38 @@
using System.Collections.Concurrent;
using Furniture.DAL.Caching;
using Furniture.DAL.Contracts;
using Furniture.DAL.Providers;
namespace Furniture.DAL.Repositories;
/// <summary>
/// Default furniture repository implementation composed from DAL providers.
/// </summary>
public sealed class FurnitureRepository(
IFurnitureDataProvider furnitureDataProvider,
ICacheInvalidationPolicy cacheInvalidationPolicy) : IFurnitureRepository
{
private readonly ConcurrentDictionary<string, FurnitureAvailabilityRecord> availabilityCache = new(
StringComparer.OrdinalIgnoreCase);
/// <inheritdoc />
public async Task<FurnitureAvailabilityRecord?> 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;
}
}

View File

@ -7,6 +7,7 @@
</PropertyGroup>
<ItemGroup>
<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="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />

View File

@ -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<IFurnitureRepository>();
var catalogRepository = provider.GetRequiredService<ICatalogRepository>();
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<IFurnitureDalGrpcContractAdapter>();
var projectionAdapter = provider.GetRequiredService<ICatalogProjectionContractAdapter>();
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<IDalDependencyHealthCheck>();
var status = await healthCheck.CheckAsync();
Assert.True(status.IsHealthy);
Assert.Contains("IFurnitureDataProvider", status.DependencyNames);
Assert.Contains("ICatalogRepository", status.DependencyNames);
}
}